• libco源码解析---co_eventloop


    引言

      我们总能在运行libco协程代码的最后看到对于函数co_eventloop的调用,它可以理解为主协程执行的函数。我们举一个简单的例子来说明它的作用:

     1 void* routinefun(void* args){
     2     co_enable_hook_sys();
     3     while(true){
     4         poll(NULL, 0, 1000);
     5     }
     6     return 0;
     7 }
     8 
     9 int main(int argc,char *argv[])
    10 {
    11     vector<task_t> v;
    12     for(int i=1;i<argc;i+=2)
    13     {
    14         task_t task = { 0 };
    15         SetAddr( argv[i],atoi(argv[i+1]),task.addr );
    16         v.push_back( task );
    17     }
    18 
    19     for(int i=0;i<2;i++)
    20     {
    21         stCoRoutine_t *co = 0;
    22         co_create( &co,NULL,routinefun,v2 );
    23         printf("routine i %d
    ",i);
    24         co_resume( co );
    25     }
    26 
    27     co_eventloop( co_get_epoll_ct(),0,0 );
    28 
    29     return 0;
    30 }

      这段代码非常简单,主协程运行两个协程,协程函数所做的事情就是使用poll切换执行权,并在一秒后切换回来(超时)。这里线程的执行过程是这样的,我们把主协程看做A,其他两个协程看做BC。执行过程为:

    • B协程执行,使用poll把一个stPoll_t结构插入时间轮,切换执行权,回到A协程。
    • C协程执行,使用poll把一个stPoll_t结构插入时间轮,切换执行权,回到A协程。
    • 此时A协程执行Eventloop中,不停的循环,直到B协程注册的事件超时,调用回调回到B协程。
    • B协程继续执行,再次使用poll,重复第一步,回到A协程。
    • A协程继续执行Eventloop,不停的循环,直到C协程注册的事件超时,调用回调回到C协程。
    • C协程继续执行,再次使用poll,重复第二步,回到A协程。

      这样我们就可以看清楚co_eventloop到底做了什么,其实就是不停的轮询等待其他协程注册的事件成立,仅此而已。主协程就相当于libco非对称协程当中的一个特殊的调度器,负责唤醒协程B和协程C,它绝不会通过yied操作主动让出CPU,但是可以通过co_resume。唤醒另一个协程,唤醒协程的回调函数OnSignalProcessEvent或OnPollProcessEvent。源码如下:

     1 static void OnSignalProcessEvent( stTimeoutItem_t * ap )
     2 {
     3     stCoRoutine_t *co = (stCoRoutine_t*)ap->pArg;
     4     co_resume( co );
     5 }
     6 
     7 void OnPollProcessEvent( stTimeoutItem_t * ap )
     8 {
     9     stCoRoutine_t *co = (stCoRoutine_t*)ap->pArg;
    10     co_resume( co );
    11 }

    co_eventloop

    co_eventloop函数源码如下:

      1 /*
      2 * libco的核心调度
      3 * 在此处调度三种事件:
      4 * 1. 被hook的io事件,该io事件是通过co_poll_inner注册进来的
      5 * 2. 超时事件
      6 * 3. 用户主动使用poll的事件
      7 * 所以,如果用户用到了三种事件,必须得配合使用co_eventloop
      8 *
      9 * @param ctx epoll管理器
     10 * @param pfn 每轮事件循环的最后会调用该函数
     11 * @param arg pfn的参数
     12 */
     13 void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
     14 {
     15     if( !ctx->result )// 给结果集分配空间
     16     {
     17         // _EPOLL_SIZE:epoll结果集大小
     18         ctx->result =  co_epoll_res_alloc( stCoEpoll_t::_EPOLL_SIZE );
     19     }
     20     co_epoll_res *result = ctx->result;
     21 
     22 
     23     for(;;)
     24     {
     25         //调用 epoll_wait() 等待 I/O 就绪事件,为了配合时间轮⼯作,这里的 timeout设置为 1 毫秒。
     26         int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
     27 
     28         /**
     29          * 获取激活事件队列和定时超时事件的临时存放链表
     30          * 不使用局部变量的原因是epoll循环并不是元素的唯一来源.例如条件变量相关(co_routine.cpp stCoCondItem_t)
     31          */
     32         stTimeoutItemLink_t *active = (ctx->pstActiveList);
     33         stTimeoutItemLink_t *timeout = (ctx->pstTimeoutList);
     34 
     35         //初始化timeout
     36         memset( timeout,0,sizeof(stTimeoutItemLink_t) );
     37 
     38         /**
     39          * 处理返回的结果集,如果pfnPrepare不为NULL,就直接调用注册的回调函数进行处理
     40          * 否则,将其直接加入就绪事件的队列,pfnPrepare实际上就是OnPollPreparePfn函数
     41          */
     42         for(int i=0;i<ret;i++)
     43         {
     44             // 获取在co_poll_inner放入epoll_event中的stTimeoutItem_t结构体
     45             stTimeoutItem_t *item = (stTimeoutItem_t*)result->events[i].data.ptr;
     46             // 如果用户设置预处理回调的话就执行
     47             if( item->pfnPrepare )
     48             {
     49                 // 若是hook后的poll的话,会把此事件加入到active队列中,并更新一些状态
     50                 item->pfnPrepare( item,result->events[i],active );
     51             }
     52             else
     53             {
     54                 AddTail( active,item );
     55             }
     56         }
     57 
     58         //从时间轮上取出已超时的事件,放到 timeout 队列。
     59         unsigned long long now = GetTickMS();
     60         TakeAllTimeout( ctx->pTimeout,now,timeout );
     61 
     62         //遍历 timeout 队列,设置事件已超时标志(bTimeout 设为 true)。
     63         stTimeoutItem_t *lp = timeout->head;
     64         // 遍历超时链表,设置超时标志,并加入active链表
     65         while( lp )
     66         {
     67             //printf("raise timeout %p
    ",lp);
     68             lp->bTimeout = true;
     69             lp = lp->pNext;
     70         }
     71 
     72         //将 timeout 队列中事件合并到 active 队列。
     73         Join<stTimeoutItem_t,stTimeoutItemLink_t>( active,timeout );
     74 
     75         /**
     76          * 遍历 active 队列,调用⼯作协程设置的 pfnProcess() 回调函数 resume挂起的⼯作协程,
     77          * 处理对应的 I/O 或超时事件。
     78          */
     79         lp = active->head;
     80         // 开始遍历active链表
     81         while( lp )
     82         {
     83             // 在链表不为空的时候删除active的第一个元素 如果删除成功,那个元素就是lp
     84             PopHead<stTimeoutItem_t,stTimeoutItemLink_t>( active );
     85             //如果被设置为超时并且当前事件还没有到达实际设置的超时事件
     86             if (lp->bTimeout && now < lp->ullExpireTime)
     87             {
     88                 // 一种排错机制,在超时和所等待的时间内已经完成只有一个条件满足才是正确的
     89                 int ret = AddTimeout(ctx->pTimeout, lp, now);
     90                 if (!ret)//插入成功
     91                 {
     92                     //重新开始定时
     93                     lp->bTimeout = false;
     94                     lp = active->head;
     95                     continue;
     96                 }
     97             }
     98             if( lp->pfnProcess )
     99             {
    100                 lp->pfnProcess( lp );
    101             }
    102 
    103             lp = active->head;
    104         }
    105         // 每次事件循环结束以后执行该函数, 用于终止协程,相当于终止回调函数
    106         if( pfn )
    107         {
    108             if( -1 == pfn( arg ) )
    109             {
    110                 break;
    111             }
    112         }
    113 
    114     }
    115 }

      首先我们可以看到active和timeout链表都在stCoEpoll_t中存储,而这个结构是线程私有的。那么为什么不把这个值设置成局部变量呢?答案不在co_eventloop中,而藏在其他函数,比如libco实现的条件变量中,条件变量会在signal后把值放入到active链表或者timeout链表,而这些只能放在stCoEpoll_t中。

    stCoEpoll_t函数源码:

     1 struct stCoEpoll_t
     2 {
     3     int iEpollFd;    // epoll 实例的⽂件描述符
     4 
     5     /**
     6      * 值为 10240 的整型常量。作为 epoll_wait() 系统调用的第三个参数,
     7      * 即⼀次 epoll_wait 最多返回的就绪事件个数。
     8      */
     9     static const int _EPOLL_SIZE = 1024 * 10;
    10 
    11     /**
    12      * 该结构实际上是⼀个时间轮(Timingwheel)定时器,只是命名比较怪,让⼈摸不着头脑。
    13      * 单级时间轮来处理其内部的超时事件。
    14      */
    15     struct stTimeout_t *pTimeout;
    16 
    17     /**
    18      * 该指针实际上是⼀个链表头。链表用于临时存放超时事件的 item。
    19      */
    20     struct stTimeoutItemLink_t *pstTimeoutList;
    21 
    22     /**
    23      * 也是指向⼀个链表。该链表用于存放 epoll_wait 得到的就绪事件和定时器超时事件。
    24      */
    25     struct stTimeoutItemLink_t *pstActiveList;
    26 
    27     /**
    28      * 对 epoll_wait()第⼆个参数的封装,即⼀次 epoll_wait 得到的结果集。
    29      */
    30     co_epoll_res *result;
    31 
    32 };

      还有这里的timeout链表其实最终会合并到active中,先分开纯粹是为了处理方便一点。然后就是把事件从epoll结果集中拿出来,如果需要的话就去执行预处理回调。我们来看看预处理回调,我们曾在poll中提到过

     1 void OnPollPreparePfn( stTimeoutItem_t * ap,struct epoll_event &e,stTimeoutItemLink_t *active )
     2 {
     3     stPollItem_t *lp = (stPollItem_t *)ap;
     4     // 把epoll此次触发的事件转换成poll中的事件
     5     lp->pSelf->revents = EpollEvent2Poll( e.events );
     6 
     7 
     8     stPoll_t *pPoll = lp->pPoll;
     9     // 已经触发的事件数加一
    10     pPoll->iRaiseCnt++;
    11 
    12     // 若此事件还未被触发过
    13     if( !pPoll->iAllEventDetach )
    14     {
    15         // 设置已经被触发的标志
    16         pPoll->iAllEventDetach = 1;
    17 
    18         // 将该事件从时间轮中移除
    19         // 因为事件已经触发了,肯定不能再超时了
    20         RemoveFromLink<stTimeoutItem_t,stTimeoutItemLink_t>( pPoll );
    21 
    22         // 将该事件添加到active列表中
    23         AddTail( active,pPoll );
    24 
    25     }
    26 }

    所做的事情:

    • 将触发的事件由epoll类型转换为poll类型
    • 触发的事件数加1
    • 如果当前事件是第一次被触发,就设置触发标志位,并将其从超时事件队列中移除,最后将其加入就绪事件队列active,如果不是第一次触发,就不再进行任何处理

    其实所做的事情就是把epoll事件对应的stPoll_t结构中的值执行一些修改,并把此项插入到active链表中。

    然后就是从时间轮中取出根据目前时间来说已经超时的事件,并插入到timeout链表中:

     1 inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
     2 {
     3     // 第一次调用是设置初始时间
     4     if( apTimeout->ullStart == 0 )
     5     {
     6         apTimeout->ullStart = allNow;
     7         apTimeout->llStartIdx = 0;
     8     }
     9 
    10     // 当前时间小于初始时间显然是有问题的
    11     if( allNow < apTimeout->ullStart )
    12     {
    13         return ;
    14     }
    15     // 求一个取出事件的有效区间
    16     int cnt = allNow - apTimeout->ullStart + 1;
    17     if( cnt > apTimeout->iItemSize )
    18     {
    19         cnt = apTimeout->iItemSize;
    20     }
    21     if( cnt < 0 )
    22     {
    23         return;
    24     }
    25     for( int i = 0;i<cnt;i++)
    26     {    // 把上面求的有效区间过一遍,某一项存在数据的话插入到超时链表中
    27         int idx = ( apTimeout->llStartIdx + i) % apTimeout->iItemSize;
    28         // 链表操作,没什么可说的
    29         Join<stTimeoutItem_t,stTimeoutItemLink_t>( apResult,apTimeout->pItems + idx  );
    30     }
    31     // 更新时间轮属性
    32     apTimeout->ullStart = allNow;
    33     apTimeout->llStartIdx += cnt - 1;
    34 }

      然后就是把超时链表处理以后加入到active链表啦。

      然后就是遍历active链表,一一执行每一个事件的回调啦,当然没执行一次回调就意味着一次协程的切换,因为我们在poll中注册的回调执行co_resume。

      循环的最后调用了pfn,这是一个我们在调用co_eventloop时传入的函数指针,它的作用是什么呢?跳出Eventloop循环的时候使用,因为不是所有的协程使用都想例子一样把 co_eventloop放在函数最后,协程更多的是嵌到代码中,我们需要在有些时候终止eventloop,传入一个终止回调就是一个不错的方法。

    co_eventloop完成的工作总结:

    1、监听所有事件

    2、如果必要的话,对就绪事件做一些预处理,再将其加入就绪事件队列,否则直接加入就绪事件队列

    3、获取超时事件

    4、将超时事件合并到就绪事件队列

    5、遍历整个就绪事件队列,调用相应的回调函数,唤醒相应的协程对改事件进行处理

    6、如果需要跳出循环的话就跳出循环,否则继续执行epoll_wait开始监听事件

    转载:

    https://blog.csdn.net/weixin_43705457/article/details/106891077?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162694514116780271590368%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162694514116780271590368&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-1-106891077.pc_search_result_cache&utm_term=libco+co_eventloop&spm=1018.2226.3001.4187

    本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15046793.html

  • 相关阅读:
    第一百一十一节,JavaScript,BOM浏览器对象模型
    第一百一十节,JavaScript匿名函数和闭包
    第一百零九节,JavaScript面向对象与原型
    第一百零八节,JavaScript,内置对象,Global对象字符串编码解码,Math对象数学公式
    第一百零七节,JavaScript基本包装类型,数据类型的方法
    第一百零六节,JavaScript变量作用域及内存
    第一百零五节,JavaScript正则表达式
    第一百零四节,JavaScript时间与日期
    经典的图片上传并绘制缩略图的类的代码
    ecshop判断搜索引擎是否为蜘蛛
  • 原文地址:https://www.cnblogs.com/MrLiuZF/p/15046793.html
Copyright © 2020-2023  润新知