0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

定时器的实现数据结构选择

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-13 14:22 次阅读

在后端的开发中,定时器有很广泛的应用。

比如:

心跳检测

倒计时

游戏开发的技能冷却

redis的键值的有效期等等,都会使用到定时器。

定时器的实现数据结构选择

红黑树

对于增删查,时间复杂度为O(logn),对于红黑树最⼩节点为最左侧节点,时间复杂度O(logn)

最小堆

对于增查,时间复杂度为O(logn),对于删时间复杂度为O(n),但是可以通过辅助数据结构( map 或者hashtable来快速索引节点)来加快删除操作;对于最⼩节点为根节点,时间复杂度为O(1)

跳表

对于增删查,时间复杂度为O(logn),对于跳表最⼩节点为最左侧节点,时间复杂度为O(1),但是空间复杂度⽐较⾼,为O(1.5n)

时间轮

对于增删查,时间复杂度为O(1),查找最⼩节点也为O(1)

libco的使用了时间轮的实现

首先,时间轮有几个结构,必须理清他们的关系。

struct stTimeoutItem_t
{
	enum { eMaxTimeout = 40 * 1000 };	// 40s
	stTimeoutItem_t* pPrev;				// 前
	stTimeoutItem_t* pNext;				// 后
	stTimeoutItemLink_t* pLink;			// 链表,没有用到,写这里有毛用

	OnPreparePfn_t pfnPrepare;			// 不是超时的事件的处理函数
	OnProcessPfn_t pfnProcess;			// resume协程回调函数

	void* pArg;							// routine 协程对象指针
	bool bTimeout;						// 是否超时
	unsigned long long ullExpireTime;	// 到期时间
};

struct stPoll_t;
struct stPollItem_t : public stTimeoutItem_t
{
	struct pollfd* pSelf;			// 对应的poll结构
	stPoll_t* pPoll;				// 所属的stPoll_t
	struct epoll_event stEvent;		// epoll事件,poll转换过来的
};

// co_poll_inner 创建,管理这多个stPollItem_t
struct stPoll_t : public stTimeoutItem_t
{
	struct pollfd* fds;				// poll 的fd集合
	nfds_t nfds;					// poll 事件个数
	stPollItem_t* pPollItems;		// 要加入epoll 事件
	int iAllEventDetach;			// 如果处理过该对象的子项目pPollItems,赋值为1
	int iEpollFd;					// epoll fd句柄
	int iRaiseCnt;					// 此次触发的事件数
};

我把这几个结构拉一起了,

图片

其中,能看出,stCoEpool_t管理了这一切

// TimeoutItem的链表
struct stTimeoutItemLink_t
{
	stTimeoutItem_t* head;
	stTimeoutItem_t* tail;
};

// TimeOut 
struct stTimeout_t	// 时间伦
{
	stTimeoutItemLink_t* pItems;	// 时间轮链表,开始初始化分配只一圈的长度,后续直接使用
	int iItemSize;					// 超时链表中一圈的tick 60*1000
	unsigned long long ullStart;	// 时间轮开始时间,会一直变化
	long long llStartIdx;			// 时间轮开始的下标,会一直变化
};

// epoll 结构
struct stCoEpoll_t
{
	int iEpollFd;
	static const int _EPOLL_SIZE = 1024 * 10;
	struct stTimeout_t* pTimeout;					// epoll 存着时间轮
	struct stTimeoutItemLink_t* pstTimeoutList;		// 超时事件链表
	struct stTimeoutItemLink_t* pstActiveList;		// 用于signal时会插入
	co_epoll_res* result;
};

也就是说,一个协程,就有一个,在co_init_curr_thread_env 中创建

它管理着超时链表,信号事件链表

其中的pTimeout,就是时间轮,也就是一个数组,这个数组的大小位60*1000

图片

stTimeout_t *AllocTimeout( int iSize )
{
	stTimeout_t *lp = (stTimeout_t*)calloc( 1,sizeof(stTimeout_t) );
	lp- >iItemSize = iSize;
	// 注意这里先把item分配好了,后续直接使用
	lp- >pItems = (stTimeoutItemLink_t*)calloc( 1, sizeof(stTimeoutItemLink_t) * lp- >iItemSize );
	lp- >ullStart = GetTickMS();
	lp- >llStartIdx = 0;
	return lp;
}

这就是分配的时间轮的方法,首先指定了下标时间等信息,根据结构注释应该不难懂

// apTimeout:时间轮
// apItem: 某一个定时item
// allNow:当前的时间
// 函数目的,将超时项apItem加入到apTimeout
int AddTimeout( stTimeout_t *apTimeout, stTimeoutItem_t *apItem ,unsigned long long allNow )
{
// 这个判断有点多余,start正常已经分配了
if( apTimeout->ullStart == 0 )
{
apTimeout->ullStart = allNow;
apTimeout->llStartIdx = 0;
}
// 当前时间也不大可能比前面的时间大
if( allNow < apTimeout->ullStart )
{
co_log_err("CO_ERR: AddTimeout line %d allNow %llu apTimeout->ullStart %llu",
LINE ,allNow,apTimeout->ullStart);

return __LINE__;
}
if( apItem- >ullExpireTime < allNow )
{
	co_log_err("CO_ERR: AddTimeout line %d apItem- >ullExpireTime %llu allNow %llu apTimeout- >ullStart %llu",
				__LINE__,apItem- >ullExpireTime,allNow,apTimeout- >ullStart);

	return __LINE__;
}
// 到期时间到start的时间差
unsigned long long diff = apItem- >ullExpireTime - apTimeout- >ullStart;
// itemsize 实际上是毫秒数,如果超出了,说明设置的超时时间过长
if( diff >= (unsigned long long)apTimeout- >iItemSize )
{
	diff = apTimeout- >iItemSize - 1;
	co_log_err("CO_ERR: AddTimeout line %d diff %d",
				__LINE__,diff);

	//return __LINE__;
}
// 将apItem加到末尾
AddTail( apTimeout- >pItems + ( apTimeout- >llStartIdx + diff ) % apTimeout- >iItemSize , apItem );

return 0;

}

其实,这里有个概念,stTimeoutItemLink_tstTimeoutItem_t,也就是说,stTimeout_t里面管理的时60*1000个链表,而每个链表有一个或者多个stTimeoutItem_t,下面这个函数,就是把节点Item加入到链表的方法。

template
void inline AddTail(TLink*apLink, TNode ap)
{
if( ap->pLink )
{
return ;
}
if(apLink->tail)
{
apLink->tail->pNext = (TNode
)ap;
ap->pNext = NULL;
ap->pPrev = apLink->tail;
apLink->tail = ap;
}
else
{
apLink->head = apLink->tail = ap;
ap->pNext = ap->pPrev = NULL;
}
ap->pLink = apLink;
}

![图片](//file1.elecfans.com/web2/M00/AD/ED/wKgaomVRwIaAdU0AAADdUXMLTLQ483.jpg)

到这里,基本把一个超时事件添加到时间轮中了,这时就应该切换协程了co_yield_env

int ret = AddTimeout( ctx->pTimeout, &arg, now );
int iRaiseCnt = 0;
if( ret != 0 )
{
co_log_err("CO_ERR: AddTimeout ret %d now %lld timeout %d arg.ullExpireTime %lld",
ret,now,timeout,arg.ullExpireTime);
errno = EINVAL;
iRaiseCnt = -1;
}
else
{
co_yield_env( co_get_curr_thread_env() );
iRaiseCnt = arg.iRaiseCnt;
}

接下来,看怎么检测超时事件co_eventloop

for(;;)
{
// 等待事件或超时1ms
int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );

//  遍历所有ret事件处理
	for(int i=0;i< ret;i++)
	{
		pfnPrepare(xxx)
	}

	// 取出所有的超时时间item,设置为超时
	TakeAllTimeout( ctx- >pTimeout, now, plsTimeout );
	stTimeoutItem_t *lp = plsTimeout- >head;
	while( lp )
	{
		lp- >bTimeout = true;
		lp = lp- >pNext;
	}

	// 将超时链表plsTimeout加入到plsActive
	Join< stTimeoutItem_t, stTimeoutItemLink_t >( plsActive, plsTimeout );
	lp = plsActive- >head;
	while( lp )
	{
        // 弹出链表头,处理超时事件
		PopHead< stTimeoutItem_t,stTimeoutItemLink_t >( plsActive );
        if (lp- >bTimeout && now < lp- >ullExpireTime) 
		{
			int ret = AddTimeout(ctx- >pTimeout, lp, now);
			if (!ret) 
			{
				lp- >bTimeout = false;
				lp = plsActive- >head;
				continue;
			}
		}
        // 只有stPool_t 才需要切协程,要切回去了
		if( lp- >pfnProcess )
		{
			lp- >pfnProcess( lp );
		}
		lp = plsActive- >head;
	}

	// 如果传入该函数指针,则可以控制event_loop 退出
	if( pfn )
	{
		if( -1 == pfn( arg ) )
		{
			break;
		}
	}
}
其中包括了定时事件处理,协程切换,主协程退出等操作。如果设置了主协程退出函数,则主协程可以正常的退出。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 定时器
    +关注

    关注

    23

    文章

    3247

    浏览量

    114761
  • 数据结构
    +关注

    关注

    3

    文章

    573

    浏览量

    40127
  • 数组
    +关注

    关注

    1

    文章

    417

    浏览量

    25943
  • Redis
    +关注

    关注

    0

    文章

    374

    浏览量

    10872
收藏 人收藏

    评论

    相关推荐

    软件定时器的特点和原理

    本文介绍了软件定时器的特点和原理,并从时钟节拍,数据结构定时器操作等角度分析,实现了基于STM32的软件定时器,该软件
    发表于 08-19 08:29

    什么是软件定时器?基于STM32的软件定时器该怎样去实现

    目录1.什么是软件定时器2.软件定时器实现原理3.基于STM32的软件定时器3.1 时钟节拍3.2 数据结构3.3
    发表于 12-22 07:47

    GPIB命令的数据结构

    针对GPIB命令的结构,提出一种存储GPIB命令的数据结构。根据GPIB命令的层次关系的特点,选择数据结构中“树”的概念来存储GPIB命令结点;并考虑程序
    发表于 02-10 16:20 70次下载

    GPIB命令的数据结构

    针对GPIB命令的结构,提出一种存储GPIB命令的数据结构。根据GPIB命令的层次关系的特点,选择数据结构中“树”的概念来存储GPIB命令结点;并考虑程序
    发表于 01-04 10:13 0次下载

    定时器结构及工作模式

    定时器是单片机的重要功能模块之一,在检测、控制领域有广泛应用。 定时器常用作定时时钟,以实现定时检测、
    发表于 09-25 10:08 4次下载
    <b class='flag-5'>定时器</b>的<b class='flag-5'>结构</b>及工作模式

    数据结构是什么_数据结构有什么用

    数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择
    发表于 11-17 14:45 1.6w次阅读
    <b class='flag-5'>数据结构</b>是什么_<b class='flag-5'>数据结构</b>有什么用

    降低了CPU负载率的μC/OS-II定时器有效改进方法

    对μC/OS-II的定时器管理算法进行改进的主要目标是:要么不对定时器进行检查,要检查则一定有定时器到期[2]。为了达到这个设计目标,需要对μC/OS-II的定时器轮进行重新设计。采用
    发表于 07-19 07:06 947次阅读
    降低了CPU负载率的μC/OS-II<b class='flag-5'>定时器</b>有效改进方法

    什么是数据结构?为什么要学习数据结构数据结构的应用实例分析

    本文档的主要内容详细介绍的是什么是数据结构?为什么要学习数据结构数据结构的应用实例分析包括了:数据结构在串口通信当中的应用,数据结构在按键
    发表于 09-26 15:45 14次下载
    什么是<b class='flag-5'>数据结构</b>?为什么要学习<b class='flag-5'>数据结构</b>?<b class='flag-5'>数据结构</b>的应用实例分析

    STM32基于cubeMX实现定时器点灯

    Cortex M3内核当中的定时器,它并不属于芯片厂商的外设,也就是说使用ARM内核的不同厂商,都拥有基本结构相同的系统定时器。主要目的是给RTOS提供时钟节拍做时间基准。基本定时器
    发表于 11-23 18:21 19次下载
    STM32基于cubeMX<b class='flag-5'>实现</b><b class='flag-5'>定时器</b>点灯

    STM32定时器-基本定时器

    目录定时器分类基本定时器功能框图讲解基本定时器功能时钟源计数时钟计数自动重装载寄存
    发表于 11-23 18:21 31次下载
    STM32<b class='flag-5'>定时器</b>-基本<b class='flag-5'>定时器</b>

    定时器作用及实现定时器数据结构选取介绍1

    定时器在各种场景都需要用到,比如游戏的Buff实现,Redis中的过期任务,Linux中的定时任务等等。顾名思义,定时器的主要用途是执行定时
    的头像 发表于 04-21 15:20 1198次阅读
    <b class='flag-5'>定时器</b>作用及<b class='flag-5'>实现</b><b class='flag-5'>定时器</b><b class='flag-5'>数据结构</b>选取介绍1

    定时器作用及实现定时器数据结构选取介绍2

    定时器在各种场景都需要用到,比如游戏的Buff实现,Redis中的过期任务,Linux中的定时任务等等。顾名思义,定时器的主要用途是执行定时
    的头像 发表于 04-21 15:20 1191次阅读
    <b class='flag-5'>定时器</b>作用及<b class='flag-5'>实现</b><b class='flag-5'>定时器</b><b class='flag-5'>数据结构</b>选取介绍2

    STM32如何使用定时器实现微秒(us)级延时?

    如何使用定时器实现微秒级延时的步骤: 步骤 1:配置定时器 首先,需要选择一个适合的定时器。大多数STM32微控制
    的头像 发表于 11-06 11:05 6298次阅读

    定时器设计实现

    返回ITimer类型的共享指针。其中ITimer类中定义了start和stop方法,用于启动或停止当前定时器。 TimerManager还有一个内部类TimerMessageQueue用于实现
    的头像 发表于 11-08 16:50 602次阅读

    redis数据结构的底层实现

    Redis是一种内存键值数据库,常用于缓存、消息队列、实时数据分析等场景。它的高性能得益于其精心设计的数据结构和底层实现。本文将详细介绍Redis常用的
    的头像 发表于 12-05 10:14 619次阅读