构建C协程之概述

本文详细解析了协程的概念、特点及其在并发系统中的应用,着重介绍了基于C语言实现协程的主要方案,包括利用setjmp/long_jmp机制、ucontext机制和C的switch/goto语句的组合,探讨了不同实现方式的优缺点,以及如何构建高效的并发系统。
摘要由CSDN通过智能技术生成
什么是协程

协程“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 敬请关注!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值