C/C++协程学习笔记丨C/C++实现协程及原理分析视频( 六 )


};
co_poll 实际是对函数 co_poll_inner 的封装 。 我们将 co_epoll_inner 函数的结构分为上下两半段 。 在上半段中 , 调用 co_poll 的协程 CC 将其需要监听的句柄数组 fds 都加入到 Epoll 管理中 , 并通过函数 co_yield_env 让出 CPU;当 main 协程的事件循环 co_eventloop 中触发了 CC 对应的监听事件时 , 会恢复 CC的执行 。 此时 , CC 将开始执行下半段 , 即将上半段添加的句柄 fds 从 epoll 中移除 , 清理残留的数据结构 , 下面的流程图简要说明了控制流的转移过程:
C/C++协程学习笔记丨C/C++实现协程及原理分析视频文章插图
有了上面的基本概念 , 我们来看具体的实现细节 。 co_poll 首先在内部将传入的文件句柄数组 fds 转化为数据结构 stPoll_t , 这一步主要是为了方便后续处理 。 该结构记录了 iEpollFd , ndfs , fds 数组 , 以及该协程需要执行的函数和参数 。 有两点需要说明的是:

  1. 对于每一个 fd , 为其申请一个 stPollItem_t 来管理对应 Epoll 事件以及记录回调参数 。 libco 在此做了一个小的优化 , 对于长度小于 2 的 fds 数组 , 直接在栈上定义相应的 stPollItem_t 数组 , 否则从堆中申请内存 。 这也是一种比较常见的优化 , 毕竟从堆中申请内存比较耗时;
  2. 函数指针 OnPollProcessEvent 封装了协程的切换过程 。 当传入指定的 stPollItem_t 结构时 , 即可唤醒对应于该结构的 coroutine , 将控制权交由其执行;
co_poll 的第二步 , 也是最关键的一步 , 就是将 fd 数组全部加入到 Epoll 中进行监听 。 协程 CC 会将每一个 epoll_event 的 data.ptr 域设置为对应的 stPollItem_t 结构 。 这样当事件触发时 , 可以直接从对应的 ptr中取出 stPollItem_t 结构 , 然后唤醒指定协程 。
如果本次操作提供了 Timeout 参数 , co_poll 还会将协程 CC 本次操作对应的 stPoll_t 加入到定时器队列中 。 这表明在 Timeout 定时触发之后 , 也会唤醒协程 CC 的执行 。 当整个上半段都完成后 , co_poll 立即调用 co_yield_env 让出 CPU , 执行流程跳转回到 main 协程中 。
从上面的流程图中也可以看出 , 当执行流程再次跳回时 , 表明协程 CC 添加的读写等监听事件已经触发 , 即可以执行相应的读写操作了 。 此时 CC 首先将其在上半段中添加的监听事件从 Epoll 中删除 , 清理残留的数据结构 , 然后调用读写逻辑 。
定时器实现
协程 CC 在将一组 fds 加入 Epoll 的同时 , 还能为其设置一个超时时间 。 在超时时间到期时 , 也会再次唤醒 CC 来执行 。 libco 使用 Timing-Wheel 来实现定时器 。 关于 Timing-Wheel 算法 , 可以参考 , 其优势是 O(1) 的插入和删除复杂度 , 缺点是只有有限的长度 , 在某些场合下不能满足需求 。
C/C++协程学习笔记丨C/C++实现协程及原理分析视频文章插图
回过去看 stCoEpoll_t 结构 , 其中 *pTimeout 代表时间轮 , 通过函数 AllocateTimeout 初始化为一个固定大小(60 * 1000)的数组 。 根据 Timing-Wheel 的特性可知 , libco 只支持最大 60s 的定时事件 。 而实际上 , 在添加定时器时 , libco 要求定时时间不超过 40s 。 成员 pstTimeoutList 记录在 co_eventloop 中发生超时的事件 , 而 pstActiveList 记录当前活跃的事件 , 包括超时事件 。 这两个结构都将在 co_eventloop 中进行处理 。
下面我们简要分析一下加入定时器的实现:
int AddTimeout( stTimeout_t *apTimeout, stTimeoutItem_t *apItem,
unsigned long long allNow )
{
if( apTimeout->ullStart == 0 ) // 初始化时间轮的基准时间
{
apTimeout->ullStart = allNow;


推荐阅读