c语言实现的协程

这几天突然对协程感兴趣,于是自己实现了一个,代码放在github上:https://github.com/adinosaur/Coro

协程是一种用户空间的非抢占式线程,主要用来解决等待大量的IO操作的问题。

协程vs线程

对比使用多线程来解决IO阻塞任务,使用协程的好处是不用加锁,访问共享的数据不用进行同步操作。这里需要说明的一点是,使用协程之所以不需要加锁不是因为所有的协程只在一个线程中运行,而是因为协程的非抢占式的特点。也就是说,使用协程的话,在没主动交出CPU之前都是不会被突然切换到其它协程上的。而线程是抢占式的,使用多线程你是不能确定你的线程什么时候被操作系统调度,什么时候被切换,因此需要用锁到实现一种“原子操作”的语义。

协程vs异步回调

其实更一般更常见的做法是,使用非阻塞的IO(比如是异步IO,又或者是在syscall上自己实现的一套异步IO,如asio)并且将处理操作写在回调函数中。这样的做法一般没什么问题,但当回调函数变多,一段连贯的业务代码就会被拆分到多个回调函数之中,增加维护的成本。因此使用协程可以用同步的写法写出效果相当于是异步的代码。

利用static变量实现协程

要实现一个协程,主要的问题是如何保存函数调用的上下文。之前在网上看到一篇博客coroutines in c,用一种非常简洁的方式实现了这个保存上下文的功能。实现代码如下:

 1 #define crBegin static int _cr_state = 0; switch(_cr_state) { case 0:
 2 #define crReturn(x) do { _cr_state = __LINE__; return x; case __LINE__:; } while (0)
 3 #define crFinish }
 4
 5 int func1() {
 6     crBegin
 7     while (1)
 8     {
 9         printf("hello world\n");
10         crReturn(0);
11     }
12     crFinish
13 }

这个代码利用了函数的static变量来保存函数调用状态。注意,由于vs2013有一个调试特性,所以vs2013的__LINE__的实现不是常量因此会编译不通过,使用gcc就可以编译。这段代码简单是简单但是有问题,比如说如果两个协程调用同一个函数,就会出错。因此博客里面提及这段代码主要是给出一个思路,如果实际使用的话这样子肯定是不行的。

利用setjmp、longjmp实现协程

前面说过,实现协程最主要的是保存函数的调用的上下文,而这些上下文主要就两个部分:1.各个寄存器的值,2.函数调用栈。C语言里可以通过setjmp来保存函数调用时,各寄存器的值。保存之后,便可以通过longjmp重现回到当初setjmp的地方(可以理解成跨函数的goto)。但是,需要注意的是,setjmp仅负责保存寄存器的值,不负责维护其函数调用栈(这个看看setjmp的jmp_buf的结构就知道了),因此必须由使用者来手动的维护这个函数调用栈。使用setjmp、longjmp的一个常见的错误就是,尝试去longjmp到一个已经执行完的函数,这时候虽然寄存器的值是当时保存的值,但是调用栈已经不是原来的调用栈了。

而我的做法是,在创建一个协程的时候在堆上申请一块空间(大小为2M)作为协程的调用栈,然后在setjmp的时候,手动更改寄存器esp的值,使其指向这个我自己创建的调用栈。因此在以后运行的时候,这个协程就会使用我提供的那块内存作为栈。

我的这个协程库提供了三个接口:

  1. coro_new:创建一个协程
  2. coro_yield:将控制权返回给调度协程
  3. coro_main:运行调度协程

协程的控制流程如下:

  1. 通过coro_main运行调度协程,并找出下一个运行的协程,运行之。
  2. 运行这个协程直到其调用coro_yield将控制权返还给调度协程。
  3. 重复以上两个步骤,直到所有协程运行完毕。

这个协程库实现的非常简单,只有100来行的代码,当然实现它的目的是为了提供一个最简单的协程模型,而不是一个功能完整、鲁棒性强的能投入实际业务运行的协程。

因此问题还是有很多的:

  1. 比如当在协程里面调用栈超过2M时,这个是需要处理的,现在的代码是没有做的,理应中断程序,避免写坏堆,产生随机的不可重现的问题。
  2. 显然在实现时没有考虑到多线程,如果在多线程环境里面运行,需要代码做同步处理。
  3. 现在的这个版本的协程有一个约定,在协程里调用的函数不能阻塞在syscall,这显然也是不科学的。一个完整的协程库,应该包含一些常用的syscall的非阻塞的实现,毕竟只有一个线程不能真的阻塞在这个调用上。
  4. 不同平台的jmp_buf实现可能不一致,因此在其它平台上需要稍微改写代码。

总结

当然实现协程还有比较一些更好的方法,比如如果能用glibc的ucontext库就可以基于这个库来实现,而不用自己手动管理函数调用的上下文了,如云风实现的协程库。

参考资料

  1. Coroutines in C。地址:http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
  2. CERL++。地址:https://zhuanlan.zhihu.com/p/19945225?refer=impress-your-cat
  3. C 的 coroutine 库。地址:http://blog.codingnow.com/2012/07/c_coroutine.html

CERL++

时间: 2024-12-07 23:44:42

c语言实现的协程的相关文章

spawn协程学习

对于IO密集型的程序,一般比较高效的做法是选择异步来实现,因为使用异步的方法更容易写出高效的程序.然而使用异步的话,经验较少的人往往会使自己的程序结构变得很混乱,进而导致程序的可读性变差.记得有人说过,在硬件飞速发展的现在,程序的可读性和可维护性的重要性在不断提高,甚至有一种更激进的说法,程序的可读性是第一位的.从Boost的1.54版本开始,coroutine引入了一种新型的协程,stackfull协程,之前Boost库的协程是stackless协程.对于这两者的区别,我仅知道stackles

 PHP_Yield协程从入门到精通

本文和大家分享的主要是PHP中Yield协程相关内容,一起来看看吧,希望对大家学习php有所帮助. 协程 基本概念 "协程"(Coroutine)概念最早由 Melvin Conway 于1958年提出.协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换.相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低.总的来说,协程为协同任务提供了一种运行时抽象,这种抽象非常适合于协同多任务调度和数据流处理.在现代操作系统和编程语言中,因为用户态线程切换代价比内核

ucontext-人人都可以实现的简单协程库

1.干货写在前面 协程是一种用户态的轻量级线程.本篇主要研究协程的C/C++的实现. 首先我们可以看看有哪些语言已经具备协程语义: 比较重量级的有C#.erlang.golang* 轻量级有python.lua.javascript.ruby 还有函数式的scala.scheme等. c/c++不直接支持协程语义,但有不少开源的协程库,如: Protothreads:一个"蝇量级" C 语言协程库 libco:来自腾讯的开源协程库libco介绍,官网 coroutine:云风的一个C语

coroutine协程

如果你接触过lua这种小巧的脚本语言,你就会经常接触到一个叫做协程的神奇概念.大多数脚本语言都有对协程不同程度的支持.但是大多编译语言,如C/C++,根本就不知道这样的东西存在.当然也很多人研究如何在编译语言实现协程的实现,轮子一个又一个的被发明.酷壳 这篇文章<一个“蝇量级” C 语言协程库>说的很详细,但对于文中介绍的协程库protothread,很难看的懂.风云大哥在搜索无满意结果后也重新发明轮子,实现自己版本的一个协程库<C 的 coroutine 库>, 风云版本的cor

对协程的一些理解

协程协程(coroutine)最早由Melvin Conway在1963年提出并实现,一句话定义:协程是用户态的轻量级的线程 线程和协程线程和协程经常被放在一起比较:线程一旦被创建出来,编写者是无法决定什么时候获得或者放出时间片的,是由操作系统进行统一调度的:而协程对编写者来说是可以控制切换的时机,并且切换代价比线程小,因为不需要进行内核态的切换.协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力, 但是可用通过多个(进

7Python全栈之路系列之协程

Python全栈之路系列之协程 What is the association? 与子例程一样,协程也是一种程序组件. 相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛. 协程源自Simula和Modula-2语言,但也有其他语言支持. 协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道. 来自维基百科 https://zh.wikipedia.org/wiki/协程 协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方

进程、线程、轻量级进程、协程和go中的Goroutine

一.进程 操作系统中最核心的概念是进程,分布式系统中最重要的问题是进程间通信. 进程是“程序执行的一个实例” ,担当分配系统资源的实体.进程创建必须分配一个完整的独立地址空间. 进程切换只发生在内核态,两步:1 切换页全局目录以安装一个新的地址空间 2 切换内核态堆栈和硬件上下文.  另一种说法类似:1 保存CPU环境(寄存器值.程序计数器.堆栈指针)2修改内存管理单元MMU的寄存器 3 转换后备缓冲器TLB中的地址转换缓存内容标记为无效. 二.线程 书中的定义:线程是进程的一个执行流,独立执行

(zt)Lua的多任务机制——协程(coroutine)

原帖:http://blog.csdn.net/soloist/article/details/329381 并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制.大致上有这么两种多任务技术,一种是抢占式多任务(preemptive multitasking),它让操作系统来决定何时执行哪个任务.另外一种就是协作式多任务(cooperative multitasking),它把决定权交给任务,让它们在自己认为合适的时候自愿放弃执行.这两种多任务方式各有优缺点,前者固

python 进程 线程 协程

并发与并行:并行是指两个或者多个事件在同一时刻发生:而并发是指两个或多个事件在同一时间间隔内发生.在单核CPU下的多线程其实都只是并发,不是并行. 进程是系统资源分配的最小单位,进程的出现是为了更好的利用CPU资源使到并发成为可能.进程由操作系统调度. 线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能.线程共享进程的大部分资源,并参与CPU的调度, 当然线程自己也是拥有自己的资源的,例如,栈,寄存器等等.线程由操作系统调度. 协程通