前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >也谈谈c语言的协程

也谈谈c语言的协程

原创
作者头像
mariolu
修改2019-07-22 00:03:07
2.3K0
修改2019-07-22 00:03:07
举报
文章被收录于专栏:CDN及云技术分享

一、协程写法

服务器的目的是让程序同时执行多个任务。

服务器并发场景是在程序IO密集型有优势。因为IO操作速度远没有CPU的计算速度快。程序阻塞IO将浪费大量CPU时间。程序计算密集型的,并发编程反而没有优势。

1.1 普通程序(IO同步阻塞)

一种情况accept和recv在同一个线程(nginx也是如此,但是nginx的event模型,这个例子当发送io阻塞没有使用event模型设计)

代码语言:javascript
复制
----------   --------         --------     --------------------------------    ------------------
| accept |-->| recv |-[slow]->| send | --> | recv peer's close fin packet | -->| inacitve close |
----------   --------         --------     --------------------------------    ------------------

1.2 多线程写法(减少IO)

拆散IO前的代码逻辑和 拆散IO后的代码逻辑。拆散后的IO通知需要select/epoll非阻塞多IO模型进行IO完成事件通知。假设还没有select/epoll的年代。如果是IO前和IO后都还是同一个线程处理,那么这里accept和read会占用同一片cpu时间内,需要注意到避免read过久而导致不能及时accept响应客户端请求。这时候,有像一些更多线程的模型。比如说Reactor和Proactor模型。Reactor的read部分(IO后)放在另一个线程进行。Proactor的accept和recv不放在同一个线程。 Reactor和Proactor模型。主线程只负责监听文件描述符是否有事件发生,有的话唤起工作进程,其他读写数据,接受新的连接,处理都在工作进程进行。

Proactor模型:将所有IO操作都交给主线程和内核来处理。工作线程只负责业务逻辑。

代码语言:javascript
复制
                                                 -------------------------------
                                             ----| event handler (other thread)|
                                            |MQ| -------------------------------
----------     -------------------     ------------------                          
| accept | --> | add epoll event | --> | NON-Block recv | 
----------     -------------------     ------------------

这里的改变有两点,一点是需要做代码的拆分,引入event handler,代码变得分离破碎了些。引入了MQ,多线程数据通过请求队列进行同步,但是多线程共享同一个地址空间,所以需要解决竞争问题。

1.3 协程写法(同步代码风格+IO异步)

如果又要实现IO异步通知机制,解放cpu,又要写成同步代码逻辑,那我们试试用协程序的方式写下。

代码语言:javascript
复制
--------   ------------------   -----------   -----------   ---------------   ------------   ----------------
|accept|-->|read_async begin|-->|epoll add|-->|cpu yield|-->|event handler|-->|cpu resume|-->|read_async end|
--------   ------------------   -----------   -----------   ---------------   ------------   ----------------

这里的代码改造关键点有自定义的read_async函数或者hook系统的标准调用read。逻辑替换成读操作会转化成一个epoll read event的add操作。add完出让自己的cpu控制权,此时cpu控制权又回到主协程(控制权的交还是保存并跳出当前的栈空间,但不是return操作,因为此时后续还有等read到数据的后续操作。这里会有设置一个调用栈的中断点。只有等有数据的时候恢复此调用栈到中断点位置。中断点的栈恢复是在event handler执行,又可以处理剩下的代码逻辑)。此时主协程从上一个操作出来可以accept别的请求。

二、协程实现

原本切换线程的动作使用协程

2.1 协程栈的生成:

代码语言:javascript
复制
struct async_job_st {
    async_fibre fibrectx;
    int (*func) (void *);//协程的IO程序逻辑函数,该函数可能会有IO逻辑
    void *funcargs;//相应的函数参数
    int ret;
    int status;
    ASYNC_WAIT_CTX *waitctx;
};

typedef struct async_fibre_st {
    ucontext_t fibre;//用来保存当前协程所在的栈空间,恢复该栈可以恢复该协程的运行
    jmp_buf env;//这个可能不是必须的?因为已经有了ucontext接口,不需要_setjmp/_longjmp
    int env_init;//这个不知道干嘛,感觉是为了切换ucontext和jmp两套接口
} async_fibre;

ucontext_t的创建办法(差不多都是这个固定写法)

    if (getcontext(&fibre->fibre) == 0) {//获取到一个ucontext_t对象,后续初始化该context
        fibre->fibre.uc_stack.ss_sp = OPENSSL_malloc(STACKSIZE);ucontext_t保存一个栈空间
        if (fibre->fibre.uc_stack.ss_sp != NULL) {
            fibre->fibre.uc_stack.ss_size = STACKSIZE;//此处为32KB,比栈的16M少很多
            fibre->fibre.uc_link = NULL;//设置下一个ucontext_t, 相当于一串context挨个执行过去,如果为空,则返回
            makecontext(&fibre->fibre, async_start_func, 0);//指定这个栈响应的 函数指针,即跳转到该context需要调用的PC入口
            return 1;
        }
    }

协程栈的生成包括寄存器,信号掩码,以及栈。

2.1.1 、协程池

数量动态可调节的池子。

代码语言:javascript
复制
struct async_pool_st {
    STACK_OF(ASYNC_JOB) *jobs;//池子,如果栈不为空,则get从栈中pop出可用job,release时push回stack中。如果栈为空,则new。
    size_t curr_size;//初始化池子大小。
    size_t max_size;//job数最大值
};

2.2 协程栈的切换:

async_fibre_swapcontext(old, new)

取出old->env,保存,调到新的new->env

2.3 协程栈的恢复:

async_fibre_swapcontext(new, old)

三、使用协程的特点和不足之处

  • 协程有个天生缺陷是,上下文切换(发生在用户态下),不能均匀分配在不同cpu上,损失了利用多核cpu的能力,但带来了不用加线程锁的好处。
  • 一句话原理:hack socket api,将socket变为非阻塞;通过epoll等待网络I/O事件;等待的前后分别yeild&resume
  • 当协程要阻塞的时候切换上下文,执行其他就绪的协程。一旦有协程需要IO,保存它的上下文环境,加入阻塞队列,然后从就绪队列取出下一个协程运行。待所有工作协程都陷入阻塞,通过epoll进行多路IO。
  • 虽然可以为每个连接开个线程,但连接数多时,线程太多导致性能压力,也可以开固定数目的线程池,但如果存在大量长连接,线程资源不被释放,后续的连接得不到处理。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、协程写法
    • 1.1 普通程序(IO同步阻塞)
      • 1.2 多线程写法(减少IO)
        • 1.3 协程写法(同步代码风格+IO异步)
        • 二、协程实现
          • 2.1 协程栈的生成:
            • 2.1.1 、协程池
          • 2.2 协程栈的切换:
            • 2.3 协程栈的恢复:
            • 三、使用协程的特点和不足之处
            相关产品与服务
            云服务器
            云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档