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

比如:

心跳检测

倒计时

游戏开发的技能冷却

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的使用了时间轮的实现

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


  1. struct stTimeoutItem_t
  2. {
  3. enum { eMaxTimeout = 40 * 1000 }; // 40s
  4. stTimeoutItem_t* pPrev; // 前
  5. stTimeoutItem_t* pNext; // 后
  6. stTimeoutItemLink_t* pLink; // 链表,没有用到,写这里有毛用
  7. OnPreparePfn_t pfnPrepare; // 不是超时的事件的处理函数
  8. OnProcessPfn_t pfnProcess; // resume协程回调函数
  9. void* pArg; // routine 协程对象指针
  10. bool bTimeout; // 是否超时
  11. unsigned long long ullExpireTime; // 到期时间
  12. };
  13. struct stPoll_t;
  14. struct stPollItem_t : public stTimeoutItem_t
  15. {
  16. struct pollfd* pSelf; // 对应的poll结构
  17. stPoll_t* pPoll; // 所属的stPoll_t
  18. struct epoll_event stEvent; // epoll事件,poll转换过来的
  19. };
  20. // co_poll_inner 创建,管理这多个stPollItem_t
  21. struct stPoll_t : public stTimeoutItem_t
  22. {
  23. struct pollfd* fds; // poll 的fd集合
  24. nfds_t nfds; // poll 事件个数
  25. stPollItem_t* pPollItems; // 要加入epoll 事件
  26. int iAllEventDetach; // 如果处理过该对象的子项目pPollItems,赋值为1
  27. int iEpollFd; // epoll fd句柄
  28. int iRaiseCnt; // 此次触发的事件数
  29. };

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

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


  1. // TimeoutItem的链表
  2. struct stTimeoutItemLink_t
  3. {
  4. stTimeoutItem_t* head;
  5. stTimeoutItem_t* tail;
  6. };
  7. // TimeOut
  8. struct stTimeout_t // 时间伦
  9. {
  10. stTimeoutItemLink_t* pItems; // 时间轮链表,开始初始化分配只一圈的长度,后续直接使用
  11. int iItemSize; // 超时链表中一圈的tick 60*1000
  12. unsigned long long ullStart; // 时间轮开始时间,会一直变化
  13. long long llStartIdx; // 时间轮开始的下标,会一直变化
  14. };
  15. // epoll 结构
  16. struct stCoEpoll_t
  17. {
  18. int iEpollFd;
  19. static const int _EPOLL_SIZE = 1024 * 10;
  20. struct stTimeout_t* pTimeout; // epoll 存着时间轮
  21. struct stTimeoutItemLink_t* pstTimeoutList; // 超时事件链表
  22. struct stTimeoutItemLink_t* pstActiveList; // 用于signal时会插入
  23. co_epoll_res* result;
  24. };

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

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

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


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

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

相关视频推荐

红黑树、最小堆、时间轮、跳表多种方式实现定时器

海量定时任务设计-时间轮

2023年最新技术图谱,c++后端的8个技术维度,助力你快速成为大牛

免费学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

​有了这些后,再来看看时怎么添加超时事件的


  1. // apTimeout:时间轮
  2. // apItem: 某一个定时item
  3. // allNow:当前的时间
  4. // 函数目的,将超时项apItem加入到apTimeout
  5. int AddTimeout( stTimeout_t *apTimeout, stTimeoutItem_t *apItem ,unsigned long long allNow )
  6. {
  7. // 这个判断有点多余,start正常已经分配了
  8. if( apTimeout->ullStart == 0 )
  9. {
  10. apTimeout->ullStart = allNow;
  11. apTimeout->llStartIdx = 0;
  12. }
  13. // 当前时间也不大可能比前面的时间大
  14. if( allNow < apTimeout->ullStart )
  15. {
  16. co_log_err(“CO_ERR: AddTimeout line %d allNow %llu apTimeout->ullStart %llu”,
  17. __LINE__,allNow,apTimeout->ullStart);
  18. return __LINE__;
  19. }
  20. if( apItem->ullExpireTime < allNow )
  21. {
  22. co_log_err(“CO_ERR: AddTimeout line %d apItem->ullExpireTime %llu allNow %llu apTimeout->ullStart %llu”,
  23. __LINE__,apItem->ullExpireTime,allNow,apTimeout->ullStart);
  24. return __LINE__;
  25. }
  26. // 到期时间到start的时间差
  27. unsigned long long diff = apItem->ullExpireTime – apTimeout->ullStart;
  28. // itemsize 实际上是毫秒数,如果超出了,说明设置的超时时间过长
  29. if( diff >= (unsigned long long)apTimeout->iItemSize )
  30. {
  31. diff = apTimeout->iItemSize – 1;
  32. co_log_err(“CO_ERR: AddTimeout line %d diff %d”,
  33. __LINE__,diff);
  34. //return __LINE__;
  35. }
  36. // 将apItem加到末尾
  37. AddTail( apTimeout->pItems + ( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize , apItem );
  38. return 0;
  39. }

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


  1. template <class TNode,class TLink>
  2. void inline AddTail(TLink*apLink, TNode *ap)
  3. {
  4. if( ap->pLink )
  5. {
  6. return ;
  7. }
  8. if(apLink->tail)
  9. {
  10. apLink->tail->pNext = (TNode*)ap;
  11. ap->pNext = NULL;
  12. ap->pPrev = apLink->tail;
  13. apLink->tail = ap;
  14. }
  15. else
  16. {
  17. apLink->head = apLink->tail = ap;
  18. ap->pNext = ap->pPrev = NULL;
  19. }
  20. ap->pLink = apLink;
  21. }

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


  1. int ret = AddTimeout( ctx->pTimeout, &arg, now );
  2. int iRaiseCnt = 0;
  3. if( ret != 0 )
  4. {
  5. co_log_err(“CO_ERR: AddTimeout ret %d now %lld timeout %d arg.ullExpireTime %lld”,
  6. ret,now,timeout,arg.ullExpireTime);
  7. errno = EINVAL;
  8. iRaiseCnt = -1;
  9. }
  10. else
  11. {
  12. co_yield_env( co_get_curr_thread_env() );
  13. iRaiseCnt = arg.iRaiseCnt;
  14. }

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


  1. for(;;)
  2. {
  3. // 等待事件或超时1ms
  4. int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
  5. // 遍历所有ret事件处理
  6. for(int i=0;i<ret;i++)
  7. {
  8. pfnPrepare(xxx)
  9. }
  10. // 取出所有的超时时间item,设置为超时
  11. TakeAllTimeout( ctx->pTimeout, now, plsTimeout );
  12. stTimeoutItem_t *lp = plsTimeout->head;
  13. while( lp )
  14. {
  15. lp->bTimeout = true;
  16. lp = lp->pNext;
  17. }
  18. // 将超时链表plsTimeout加入到plsActive
  19. Join<stTimeoutItem_t, stTimeoutItemLink_t>( plsActive, plsTimeout );
  20. lp = plsActive->head;
  21. while( lp )
  22. {
  23. // 弹出链表头,处理超时事件
  24. PopHead<stTimeoutItem_t,stTimeoutItemLink_t>( plsActive );
  25. if (lp->bTimeout && now < lp->ullExpireTime)
  26. {
  27. int ret = AddTimeout(ctx->pTimeout, lp, now);
  28. if (!ret)
  29. {
  30. lp->bTimeout = false;
  31. lp = plsActive->head;
  32. continue;
  33. }
  34. }
  35. // 只有stPool_t 才需要切协程,要切回去了
  36. if( lp->pfnProcess )
  37. {
  38. lp->pfnProcess( lp );
  39. }
  40. lp = plsActive->head;
  41. }
  42. // 如果传入该函数指针,则可以控制event_loop 退出
  43. if( pfn )
  44. {
  45. if( -1 == pfn( arg ) )
  46. {
  47. break;
  48. }
  49. }
  50. }

其中包括了定时事件处理,协程切换,主协程退出等操作。如果设置了主协程退出函数,则主协程可以正常的退出。