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

  • 函数指针 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;
    apTimeout->llStartIdx = 0; // 当前时间轮指针指向数组0
    }
    // 1. 当前时间不可能小于时间轮的基准时间
    // 2. 加入的定时器的超时时间不能小于当前时间
    if( allNow < apTimeout->ullStart || apItem->ullExpireTime < allNow )
    {
    return __LINE__;
    }
    int diff = apItem->ullExpireTime - apTimeout->ullStart;
    if( diff >= apTimeout->iItemSize ) // 添加的事件不能超过时间轮的大小
    {
    return __LINE__;
    }
    // 插入到时间轮盘的指定位置
    AddTail( apTimeout->pItems +
    (apTimeout->llStartIdx + diff ) % apTimeout->iItemSize, apItem );
    return 0;
    }
    定时器的超时检查在函数 co_eventloop 中执行 。
    EPOLL 事件循环main 协程通过调用函数 co_eventloop 来监听 Epoll 事件 , 并在相应的事件触发时切换到指定的协程执行 。 有关 co_eventloop 与 应用协程的交互过程在上一节的流程图中已经比较清楚了 , 下面我们主要介绍一下 co_eventloop 函数的实现:
    上文中也提到 , 通过 epoll_wait 返回的事件都保存在 stCoEpoll_t 结构的 co_epoll_res 中 。 因此 co_eventloop 首先为 co_epoll_res 申请空间 , 之后通过一个无限循环来监听所有 coroutine 添加的所有事件:
    for(;;)
    {
    int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
    ...
    }
    对于每一个触发的事件 , co_eventloop 首先通过指针域 data.ptr 取出保存的 stPollItem_t 结构 , 并将其添加到 pstActiveList 列表中;之后从定时器轮盘中取出所有已经超时的事件 , 也将其全部添加到 pstActiveList 中 , pstActiveList 中的所有事件都作为活跃事件处理 。
    对于每一个活跃事件 , co_eventloop 将通过调用对应的 pfnProcess 也就是上图中的OnPollProcessEvent 函数来切换到该事件对应的 coroutine , 将流程跳转到该 coroutine 处执行 。
    最后 co_eventloop 在调用时也提供一个额外的参数来供调用者传入一个函数指针 pfn 。 该函数将会在每次循环完成之后执行;当该函数返回 -1 时 , 将会终止整个事件循环 。 用户可以利用该函数来控制 main 协程的终止或者完成一些统计需求 。