一种协程的 C/C++ 实现

介绍

在前几天接触到了协程的概念,觉得很有趣。因为我可以使用一个线程来实现一个类似多线程的程序,如果使用协程来替代线程,就可以省去很多原子操作和内存栅栏的麻烦,大大减少与线程同步相关的系统调用。因为我只有一个线程,而且协程之间的切换是可以由函数自己决定的。

我有见过几种协程的实现,因为没有 C/C++ 的原生支持,所以多数的库使用了汇编代码,还有些库利用了 C 语言的 setjmplongjmp 但是要求函数里面使用 static local 的变量来保存协程内部的数据。我讨厌写汇编和使用 static local 变量,所以想出了一种稍微优雅一点又有点奇技淫巧的实现方法。 这篇文章将向你展示这种方法基本原理和实现。

基本原理

用 C/C++ 实现的最大困难就是创建,保存和恢复程序的上下文。因为这涉及到了程序栈的管理,以及 CPU 寄存器的访问,但是这两项内容在 C/C++ 标准里面都没有严格的定义,所以我们是不可能有一个完全跨平台的 C/C++ 实现的。但是利用操作系统提供的 API,我们仍然可以避免使用汇编代码,接下来会向你展示使用 POSIX 的 pthread 实现的一种简单的协程框架。什么!??Pthread?那你的程序岂不是多线程了?那还叫协程吗!没错,确实是多线程的,不过仅仅是在协程被创建之前的短暂瞬间。

要创建子程序的上下文,我们可以调用 pthread_create 函数来创建一个真正的线程,这样操作系统就会帮我们创建上下文(这里包括初始化 CPU 寄存器和程序栈)。然后在线程启动时,使用 C 语言的 setjmp 把这些寄存器备份到外部的 buffer 里面。创建完后,这个线程便失去了它的存在价值,所以可以果断干掉它了。不过还需要注意一点,就是在创建线程之前,需要调用 pthread_attr_setstack 函数来显式地声明使用的程序栈,这样线程退出的时候,系统就不会自动销毁这个程序栈。至于上下文的恢复,显然就是使用 longjmp 函数了。

创建上下文

下面是 RoutineInfo 的定义。为了简单起见,所有错误处理代码都被省略了,原版本的代码在 coroutine.cpp 文件中,省略版的代码在 coroutine_demonstration.cpp 文件中。

  1. typedef void * (*RoutineHandler)(void*);
  2. struct RoutineInfo{
  3. void * param;
  4. RoutineHandler handler;
  5. void * ret;
  6. bool stopped;
  7. jmp_buf buf;
  8. void *stackbase;
  9. size_t stacksize;
  10. pthread_attr_t attr;
  11. // size: the stack size
  12. RoutineInfo(size_t size){
  13. param = NULL;
  14. handler = NULL;
  15. ret = NULL;
  16. stopped = false;
  17. stackbase = malloc(size);
  18. stacksize = size;
  19. pthread_attr_init(&attr);
  20. if(stacksize)
  21. pthread_attr_setstack(&attr,stackbase,stacksize);
  22. }
  23. ~RoutineInfo(){
  24. pthread_attr_destroy(&attr);
  25. free(stackbase);
  26. }
  27. };

然后,我们需要一下全局的列表来保存这些 RoutineInfo 对象。

  1. std::list<RoutineInfo*> InitRoutines(){
  2. std::list<RoutineInfo*> list;
  3. RoutineInfo *main = new RoutineInfo(0);
  4. list.push_back(main);
  5. return list;
  6. }
  7. std::list<RoutineInfo*> routines = InitRoutines();

接下来是协程的创建,注意当协程的时候,程序栈有可能已经被损坏了,所以需要一个 stackBack 作为程序栈的备份,用来做后面的恢复。

  1. void *stackBackup = NULL;
  2. void *CoroutineStart(void *pRoutineInfo);
  3. int CreateCoroutine(RoutineHandler handler,void* param ){
  4. RoutineInfo* info = new RoutineInfo(PTHREAD_STACK_MIN+ 0x4000);
  5. info->param = param;
  6. info->handler = handler;
  7. pthread_t thread;
  8. int ret = pthread_create( &thread, &(info->attr), CoroutineStart, info);
  9. void* status;
  10. pthread_join(thread,&status);
  11. memcpy(info->stackbase,stackBackup,info->stacksize); // restore the stack
  12. routines.push_back(info); // add the routine to the end of the list
  13. return 0;
  14. }

然后是 CoroutinneStart 函数。当线程进入这个函数的时候,使用 setjmp 保存上下文,然后备份它自己的程序栈,然后直接退出线程。

  1. void Switch();
  2. void *CoroutineStart(void *pRoutineInfo){
  3. RoutineInfo& info = *(RoutineInfo*)pRoutineInfo;
  4. if( !setjmp(info.buf)){
  5. // back up the stack, and then exit
  6. stackBackup = realloc(stackBackup,info.stacksize);
  7. memcpy(stackBackup,info.stackbase, info.stacksize);
  8. pthread_exit(NULL);
  9. return (void*)0;
  10. }
  11. info.ret = info.handler(info.param);
  12. info.stopped = true;
  13. Switch(); // never return
  14. return (void*)0; // suppress compiler warning
  15. }

上下文切换

一个协程主动调用 Switch() 函数,才切换到另一个协程。

  1. std::list<RoutineInfo*> stoppedRoutines = std::list<RoutineInfo*>();
  2. void Switch(){
  3. RoutineInfo* current = routines.front();
  4. routines.pop_front();
  5. if(current->stopped){
  6. // The stack is stored in the RoutineInfo object,
  7. // delete the object later, now know
  8. stoppedRoutines.push_back(current);
  9. longjmp( (*routines.begin())->buf ,1);
  10. }
  11. routines.push_back(current); // adjust the routines to the end of list
  12. if( !setjmp(current->buf) ){
  13. longjmp( (*routines.begin())->buf ,1);
  14. }
  15. if(stoppedRoutines.size()){
  16. delete stoppedRoutines.front();
  17. stoppedRoutines.pop_front();
  18. }
  19. }

演示

用户的代码很简单,就像使用一个线程库一样,一个协程主动调用 Switch() 函数主动让出 CPU 时间给另一个协程。

  1. #include <iostream>
  2. using namespace std;
  3. #include <sys/wait.h>
  4. void* foo(void*){
  5. for(int i=0; i<2; ++i){
  6. cout<<"foo: "<<i<<endl;
  7. sleep(1);
  8. Switch();
  9. }
  10. }
  11. int main(){
  12. CreateCoroutine(foo,NULL);
  13. for(int i=0; i<6; ++i){
  14. cout<<"main: "<<i<<endl;
  15. sleep(1);
  16. Switch();
  17. }
  18. }

记得在链接的时候加上 -lpthread 链接选项。程序的执行结果如下所示:

  1. [roxma@VM_6_207_centos coroutine]$ g++ coroutime_demonstration.cpp -lpthread -o a.out
  2. [roxma@VM_6_207_centos coroutine]$ ls
  3. a.out coroutime.cpp coroutime_demonstration.cpp README.md
  4. [roxma@VM_6_207_centos coroutine]$ ./a.out
  5. main: 0
  6. foo: 0
  7. main: 1
  8. foo: 1
  9. main: 2
  10. main: 3
  11. main: 4
  12. main: 5

原文及代码下载

https://github.com/roxma/cpp_learn/tree/master/cpp/linux_programming/coroutine

一种协程的 C/C++ 实现的更多相关文章

  1. 协程coroutine

    协程(coroutine)顾名思义就是“协作的例程”(co-operative routines).跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程 ...

  2. golang协程池设计

    Why Pool go自从出生就身带“高并发”的标签,其并发编程就是由groutine实现的,因其消耗资源低,性能高效,开发成本低的特性而被广泛应用到各种场景,例如服务端开发中使用的HTTP服务,在g ...

  3. 一个“蝇量级” C 语言协程库

    协程(coroutine)顾名思义就是“协作的例程”(co-operative routines).跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程 ...

  4. day13学python 协程+事件驱动

    协程+事件驱动 协程 (微线程)--用处多,重点 当调度切换时 靠寄存器上下文和栈保存 要使用时再调用(即可不会因io传输数据卡壳 从而耗时无法继续进行)实现并行 优缺点: 优点: 1 无需同线程上下 ...

  5. python 协程与go协程的区别

    进程.线程和协程 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定义: 操作系统能够进行运算调度的最小单位.它被包含在进 ...

  6. Java协程实践指南(一)

    一. 协程产生的背景 说起协程,大多数人的第一印象可能就是GoLang,这也是Go语言非常吸引人的地方之一,它内建的并发支持.Go语言并发体系的理论是C.A.R Hoare在1978年提出的CSP(C ...

  7. web服务-2、四种方法实现并发服务器-多线程,多进程,协程,(单进程-单线程-非堵塞)

    知识点:1.使用多线程,多进程,协程完成web并发服务器 2.单进程-单线程-非堵塞也可以实现并发服务器 1.多进程和协程的代码在下面注释掉的部分,我把三种写在一起了 import socket im ...

  8. (并发编程)进程池线程池--提交任务2种方式+(异步回调)、协程--yield关键字 greenlet ,gevent模块

    一:进程池与线程池(同步,异步+回调函数)先造个池子,然后放任务为什么要用“池”:池子使用来限制并发的任务数目,限制我们的计算机在一个自己可承受的范围内去并发地执行任务池子内什么时候装进程:并发的任务 ...

  9. golang协程同步的几种方法

    目录 golang协程同步的几种方法 协程概念简要理解 为什么要做同步 协程的几种同步方法 Mutex channel WaitGroup golang协程同步的几种方法 本文简要介绍下go中协程的几 ...

随机推荐

  1. BGP详解

    相信各位站长在托管服务器或者选择虚拟主机的时候,提供商都会说他们的机房是双线机房,保证你的站点访问速度,那么这里所谓的双线机房到底是何意思,它又为何能提升站点的访问速度呢? 一遍小型机房的所谓双线路其 ...

  2. 【转】foxmail突然打不开了,双击没反应,怎么回事呀

    原文网址:http://tieba.baidu.com/p/3492526384 解决方法如下:1.进入foxmail安装目录(默认在D盘Program Files下层,右击foxmail这个文件夹, ...

  3. [Tommas] 如何创建自动化功能测试的基本原则

    每个实行持续交付的项目,都有生产流水线的元素,如持续集成和自动化测试.这些测试是在不同层面进行的,从单元测试到冒烟测试再到功能测试.自动化功能测试的优点之一是可重复性和可预测的执行时间.出于这个原因, ...

  4. [liu yanling]软件开发的过程按阶段划分有:单元测试 集成测试 系统测试 验收测试

    从软件开发的过程按阶段划分有:单元测试 集成测试 系统测试 验收测试测试过程按 4 个步骤进行,概念内容如下:单元测试:单元测试是对软件基本组成单元(如函数.类的方法等)进行的测试.集成测试:集成测试 ...

  5. Windows Azure的故障检测和重试逻辑

    高度可用的应用程序设计的一个关键点,是利用代码中的重试逻辑正常处理临时中断的服务.Microsoft 模式和实践团队开发的暂时性故障处理应用程序块可协助应用程序开发人员完成此过程.“暂时性”一词表示仅 ...

  6. Drupal安装及使用问题解决列表

    #1. 启动 Clean URL 修改Apache的配置文件(如httpd.conf),打开 LoadModule rewrite_module modules/mod_rewrite.so选项.然后 ...

  7. oracle中exp,imp的使用详解

    http://www.cnblogs.com/yugen/archive/2010/07/25/1784763.html

  8. 教程-Delphi各种退出break,continue, exit,abort, halt, runerror

    delphi中表示跳出的有break,continue, exit,abort, halt, runerror.1.break 强制退出循环(只能放在循环中),用于从For语句,while语句或rep ...

  9. hdoj 3861 The King’s Problem【强连通缩点建图&&最小路径覆盖】

    The King’s Problem Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Other ...

  10. 关于手机短信验证码存session 获取不到的问题

    问题描述:最近做一个项目,手机端注册,服务端产生一个验证码,通过短信发送到手机,并存放到session中,但手机端发送第二次请求传回验证码,要对两个验证码进行比较判断时,session存放的验证码丢失 ...