现在的位置: 首页 > 综合 > 正文

构建C协程之概述

2018年02月03日 ⁄ 综合 ⁄ 共 1658字 ⁄ 字号 评论关闭

什么是协程

协程“coroutine”),有时也叫做用户线程纤程
“fiber”)等,是一种轻量级用户执行线索,其特点是调度和切换都发生在用户态,无需内核干预,因此切换代价较小,特别适合实现一些高并发类的系统应用 ——
比如 Web
服务器 —— 每个链接的服务历程都可以用协程来实现,当某个链接遇到I/O阻塞时,可以快速切换到其他执行线索,从而大大提高了系统整体的吞吐率。

由于系统的调度器和执行线索都处于用户态,调度器通常无法中断某个运行中的协程,因此通常来说,协程的调度器往往采用协作调度策略
——
即执行中的协程
需要显式调用类似yeild 这样的方法来让出处理器资源,以便其他任务执行。这也就是协程得名的原因。

当然,对于一些语言,比如 Erlang,由于采用基于指令虚拟机的实现方式,调度器通常实现在虚拟机层,仍然能够控制用户级任务的执行,因此 Erlang
的轻量进程是采用分时调度的。但我们这里仅讨论一般意义上的协程,且主要基于C语言的实现,所以仍以协作调度为主。

类似系统

一种类似的方案是采用异步+回调的方式,比如libeventnode.js之类框架,其本质是将用户任务的粒度降低到以函数为单位,系统后台启动多个工作线程,通过事件驱动的方式异步的从任务队列中取出并执行这些回调函数。

这类方案的底层系统实现起来相对比较简单,理论上也非常高效,但要求用户程序以异步方式编写,给用户程序开发、代码维护、调试等带来了一些问题。而基于协程的系统,所有用户任务都是同步的,也就是完全按照实际执行时序编程,降低了用户程序开发、调试、维护的开销。

基于C语言的实现模型

早期的构建在C语言之上协程库往往仅包含一个执行的OS线程,多个用户任务都在该线程上分时执行,是一种
“N:1”
的映射模式。典型的例子是libtask。由于调度器实际上是串行执行的,无需考虑复杂的线程同步问题,所以实现起来就比较简单。

近年来,随着多核乃至众核处理器的大规模出现和普及,使得原来基于 “N:1”
的模型无法满足系统的可扩展性需求。因此业界提出了很多基于多核架构的协程系统方案,即所谓 “M:N”
模型 ——
多个协程可以映射到多个OS线程执行,也就是说在多核平台上,不同的协程能实现真正意义上的并行执行。

本质上来说,Google开发的Go语言和Intel主导开发的Cilkplus语言都是
“M:N”
的代表——虽然它们表面上都是新的语言,但调度器核心,即运行时环境(runtime)都是基于C(及部分C++)语言的。所以,这两个runtime系统将作为构建C协程这个系列里,重点关注的对象。

实现方式分类

总结起来,用C语言实现协程的主要方案包括三类:

1.       利用标准C提供的setjmp/long_jmp机制,比如libconcurrency,以及前面讲的Cilkplus的运行时环境均属此类。这种方式的优点是可移植性好,理论上只要平台提供C标准库就可以移植,并且协程切换效率相对比较高。但同时,对其流程把握通常比较困难,也很难为每个协程实现独立的运行栈。

2.       利用GNU C库提供的ucontext机制;或者使用Windows平台提供的Fiber机制。这种方式的优点是流程清晰,编程思路简单;但是可移植性和切换效率欠佳。

3.       利用Cswitch/goto等语句的巧妙组合,可以用少量的代码实现简单的协程支持,比如Protothreads项目,其实现非常简单,号称蝇量级。但可扩展性不好,不适合移植到多核等复杂系统,因此就不在本系列中详述了,感兴趣者请参考“Protothreads” 代码及相关分析

在后续的文章中,我会针对前两种实现进行分析,主要的参考是采用“setjmp/long_jmp”实现的 Cilkplus
运行时库 libcilkrtsLinux版),以及采用“ucontext”实现的
GCC Go
前端运行时库libgo 敬请关注!

 

抱歉!评论已关闭.