libco源码阅读(五):协程调度——时间轮

1、时间轮介绍

2、epoll和时间轮

3、epoll和时间轮的创建


    在创建线程环境的时候,会为线程环境创建一个epoll的结构体,用于协程的调度,接下来我们来一探究竟。

void co_init_curr_thread_env()
{
    ... ...

	stCoEpoll_t *ev = AllocEpoll(); // 为线程环境创建一个epoll的结构
	SetEpoll(env, ev);
}

1、时间轮介绍

      时间轮是用于实现定时器的一种算法。

         

        时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格个数是固定的,可用wheelSize来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式 tickMs × wheelSize计算得出。时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime是tickMs的整数倍。currentTime可以将整个时间轮划分为到期部分和未到期部分,currentTime当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的链表的所有任务。

    若时间轮的tickMs=1ms,wheelSize=20,那么可以计算得出interval为20ms。初始情况下表盘指针currentTime指向时间格0,此时有一个定时为2ms的任务插入进来会存放到时间格为2的链表中。随着时间的不断推移,指针currentTime不断向前推进,过了2ms之后,当到达时间格2时,就需要将时间格2所对应的链表中的任务做相应的到期操作。此时若又有一个定时为8ms的任务插入进来,则会存放到时间格10中,currentTime再过8ms后会指向时间格10。如果同时有一个定时为19ms的任务插入进来怎么办?如果此时有个定时为350ms的任务该如何处理?当任务的到期时间超过了当前时间轮所表示的时间范围时,就会尝试添加到上层时间轮中。

    参考上图,复用之前的案例,第一层的时间轮tickMs=1ms, wheelSize=20, interval=20ms。第二层的时间轮的tickMs为第一层时间轮的interval,即为20ms。每一层时间轮的wheelSize是固定的,都是20,那么第二层的时间轮的总体时间跨度interval为400ms。以此类推,这个400ms也是第三层的tickMs的大小,第三层的时间轮的总体时间跨度为8000ms。

    对于之前所说的350ms的定时任务,显然第一层时间轮不能满足条件,所以就升级到第二层时间轮中,最终被插入到第二层时间轮中时间格17所对应的链表中。如果此时又有一个定时为450ms的任务,那么显然第二层时间轮也无法满足条件,所以又升级到第三层时间轮中,最终被插入到第三层时间轮中时间格1的链表中。注意到在到期时间在[400ms,800ms)区间的多个任务(比如446ms、455ms以及473ms的定时任务)都会被放入到第三层时间轮的时间格1中,时间格1对应的链表的超时时间为400ms。随着时间的流逝,当次链表到期之时,原本定时为450ms的任务还剩下50ms的时间,还不能执行这个任务的到期操作。这里就有一个时间轮降级的操作,会将这个剩余时间为50ms的定时任务重新提交到层级时间轮中,此时第一层时间轮的总体时间跨度不够,而第二层足够,所以该任务被放到第二层时间轮到期时间为[40ms,60ms)的时间格中。再经历了40ms之后,此时这个任务又被“察觉”到,不过还剩余10ms,还是不能立即执行到期操作。所以还要再有一次时间轮的降级,此任务被添加到第一层时间轮到期时间为[10ms,11ms)的时间格中,之后再经历10ms后,此任务真正到期,最终执行相应的到期操作。

2、epoll和时间轮

struct stCoEpoll_t
{
	int iEpollFd;  // epoll 专用的文件描述符
	static const int _EPOLL_SIZE = 1024 * 10; // epoll可以监听的事件数,默认为10240,这是一个静态变量

	struct stTimeout_t *pTimeout; // 单轮时间轮,相当于一个定时器

	struct stTimeoutItemLink_t *pstTimeoutList; // 链表用于临时存放超时事件的item

	struct stTimeoutItemLink_t *pstActiveList; // 该链表用于存放epoll_wait得到的就绪事件和定时器超时事件

	co_epoll_res *result; // 对 epoll_wait()第二个参数的封装,即一次 epoll_wait 得到的结果集
};

struct co_epoll_res
{
	int size;
	struct epoll_event *events;
	struct kevent *eventlist;
};

/*
* 毫秒级的超时管理器,即一个定时器的实现
* 使用时间轮实现
* 但是是有限制的,最长超时时间不可以超过iItemSize毫秒
*/
struct stTimeout_t
{
    /*
    * 为一个链表,超时事件数组,总长度为iItemSize,每一项代表1毫秒,代表这个时间所超时的事件。
    * 这个数组在使用的过程中,会使用取模的方式,把它当做一个循环数组来使用
    */
	stTimeoutItemLink_t *pItems; 

	int iItemSize; // 数组长度,也即最长的定时时间,单位为ms

	unsigned long long ullStart; // 时间轮第一次使用的时间
	long long llStartIdx; // 目前正在使用的下标
};

struct stTimeoutItemLink_t
{
	stTimeoutItem_t *head; // 链表头指针
	stTimeoutItem_t *tail; // 链表尾指针

};

typedef void (*OnPreparePfn_t)( stTimeoutItem_t *,struct epoll_event &ev, stTimeoutItemLink_t *active );
typedef void (*OnProcessPfn_t)( stTimeoutItem_t *);
struct stTimeoutItem_t
{
	enum
	{
		eMaxTimeout = 40 * 1000 // poll注册事件时的最大超时时间为40s
	};
	stTimeoutItem_t *pPrev;  // 链表节点的前一个节点
	stTimeoutItem_t *pNext;  // 链表的节点下一个节点
	stTimeoutItemLink_t *pLink; // 该节点所属的链表

	unsigned long long ullExpireTime; // 超时时间

	OnPreparePfn_t pfnPrepare; // 预处理回调函数,在epoll_loop中被调用
	OnProcessPfn_t pfnProcess; // 正式的回调函数,在epoll_loop中被调用

	void *pArg;    // 指向协程实体
	bool bTimeout; // 是否超时
};



3、epoll和时间轮的创建

stCoEpoll_t *AllocEpoll()
{
	stCoEpoll_t *ctx = (stCoEpoll_t*)calloc( 1,sizeof(stCoEpoll_t) );

	ctx->iEpollFd = co_epoll_create( stCoEpoll_t::_EPOLL_SIZE );
	ctx->pTimeout = AllocTimeout( 60 * 1000 ); // 最大超时时间为60s
	
	ctx->pstActiveList = (stTimeoutItemLink_t*)calloc( 1,sizeof(stTimeoutItemLink_t) );
	ctx->pstTimeoutList = (stTimeoutItemLink_t*)calloc( 1,sizeof(stTimeoutItemLink_t) );


	return ctx;
}

stTimeout_t *AllocTimeout( int iSize )
{
	stTimeout_t *lp = (stTimeout_t*)calloc( 1,sizeof(stTimeout_t) );	

	lp->iItemSize = iSize;
	lp->pItems = (stTimeoutItemLink_t*)calloc( 1,sizeof(stTimeoutItemLink_t) * lp->iItemSize );

	lp->ullStart = GetTickMS();
	lp->llStartIdx = 0;

	return lp;
}

     这里的时间轮使用一个链表来实现,是一个单轮的时间轮。一般来说单轮时间轮复杂度降低的时候超时时间大于时间轮长度的时候需要取余放入,导致每次从时间轮取出的时候都会有一些无效的遍历,libco在超时时间大于时间轮长度的时候就直接拒绝了。而多轮时间轮因为其特性很难出现超时时间大于时间轮长度,所有就没有了无效遍历,但是需要一些拷贝。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值