一种协程的 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 文件中。

typedef void * (*RoutineHandler)(void*);

struct RoutineInfo{
void * param;
RoutineHandler handler;
void * ret;
bool stopped; jmp_buf buf; void *stackbase;
size_t stacksize; pthread_attr_t attr; // size: the stack size
RoutineInfo(size_t size){
param = NULL;
handler = NULL;
ret = NULL;
stopped = false; stackbase = malloc(size);
stacksize = size; pthread_attr_init(&attr);
if(stacksize)
pthread_attr_setstack(&attr,stackbase,stacksize);
} ~RoutineInfo(){
pthread_attr_destroy(&attr);
free(stackbase);
}
};

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

std::list<RoutineInfo*> InitRoutines(){
std::list<RoutineInfo*> list;
RoutineInfo *main = new RoutineInfo(0);
list.push_back(main);
return list;
}
std::list<RoutineInfo*> routines = InitRoutines();

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

void *stackBackup = NULL;
void *CoroutineStart(void *pRoutineInfo); int CreateCoroutine(RoutineHandler handler,void* param ){
RoutineInfo* info = new RoutineInfo(PTHREAD_STACK_MIN+ 0x4000); info->param = param;
info->handler = handler; pthread_t thread;
int ret = pthread_create( &thread, &(info->attr), CoroutineStart, info); void* status;
pthread_join(thread,&status); memcpy(info->stackbase,stackBackup,info->stacksize); // restore the stack routines.push_back(info); // add the routine to the end of the list return 0;
}

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

void Switch();

void *CoroutineStart(void *pRoutineInfo){

	RoutineInfo& info = *(RoutineInfo*)pRoutineInfo;

	if( !setjmp(info.buf)){
// back up the stack, and then exit
stackBackup = realloc(stackBackup,info.stacksize);
memcpy(stackBackup,info.stackbase, info.stacksize); pthread_exit(NULL); return (void*)0;
} info.ret = info.handler(info.param); info.stopped = true;
Switch(); // never return return (void*)0; // suppress compiler warning
}

上下文切换

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

std::list<RoutineInfo*> stoppedRoutines = std::list<RoutineInfo*>();

void Switch(){
RoutineInfo* current = routines.front();
routines.pop_front(); if(current->stopped){
// The stack is stored in the RoutineInfo object,
// delete the object later, now know
stoppedRoutines.push_back(current);
longjmp( (*routines.begin())->buf ,1);
} routines.push_back(current); // adjust the routines to the end of list if( !setjmp(current->buf) ){
longjmp( (*routines.begin())->buf ,1);
} if(stoppedRoutines.size()){
delete stoppedRoutines.front();
stoppedRoutines.pop_front();
}
}

演示

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

#include <iostream>
using namespace std; #include <sys/wait.h> void* foo(void*){
for(int i=0; i<2; ++i){
cout<<"foo: "<<i<<endl;
sleep(1);
Switch();
}
} int main(){
CreateCoroutine(foo,NULL);
for(int i=0; i<6; ++i){
cout<<"main: "<<i<<endl;
sleep(1);
Switch();
}
}

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

[roxma@VM_6_207_centos coroutine]$ g++ coroutime_demonstration.cpp -lpthread -o a.out
[roxma@VM_6_207_centos coroutine]$ ls
a.out coroutime.cpp coroutime_demonstration.cpp README.md
[roxma@VM_6_207_centos coroutine]$ ./a.out
main: 0
foo: 0
main: 1
foo: 1
main: 2
main: 3
main: 4
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. ☀【动画】过渡 transition

    CSS3 动画系列3-transition(过渡) √http://www.css88.com/archives/5403 如果丘处机没有路过牛家村,中国将是最发达国家 <!DOCTYPE ht ...

  2. zoj 1033 与其说是搜索,不如说是枚举

    zoj 与其说是搜索,不如说是枚举,只不过是通过搜索来实现的罢了. 主要是要注意好闰年的判断,特别是要注意好一串数字的划分. 题意其实我也看了一个晚上,才渐渐的看懂. 题意: 给你一个字符串,其中包含 ...

  3. Android学习之路——简易版微信为例(一)

    这是“Android学习之路”系列文章的开篇,可能会让大家有些失望——这篇文章中我们不介绍简易版微信的实现(不过不是标题党哦,我会在后续博文中一步步实现这个应用程序的).这里主要是和广大园友们聊聊一个 ...

  4. Android获取IMSI和IMEI

    IMSI是一个 唯一的数字, 标识了GSM和UMTS 网络里的唯一一个用户. 它 存储 在手机的SIM卡里,它会通过手机发送到网络上. IMEI也是一串唯一的数字, 标识了 GSM 和 UMTS网络里 ...

  5. Linux使用sudo提权时,出现xx 不在 sudoers 文件中。此事将被报告。visudo 命令简单介绍。

    在使用 sudo 临时提权时,出现:不在 sudoers 文件中.此事将被报告. 可以使用 visudo命令 来配置/etc/sudoers文件,将目标用户赋予使用sudo命令的能力. visudo命 ...

  6. lru cache java

    http://www.acmerblog.com/leetcode-lru-cache-lru-5745.html acm之家的讲解是在是好,丰富 import java.util.LinkedHas ...

  7. ARM--存储管理器

    初入领悟: 1. bank.L-bank的概念 2. s3c2440内部管理SDRAM寄存器配置 Frist part:原理分析 S3c2440为32位微处理器,其可访问空间为4G:但其中提供1G外设 ...

  8. 调试 Azure 云服务项目的方法

    调试 Azure 云服务项目 在菜单栏中选择“调试”>“启动调试”(键盘操作:F5). 重要提示 如果未将 Azure 云服务项目设置为启动项目,当你单击按钮以触发断点时,将出现以下错误:“外部 ...

  9. Linux 下svn恢复到某一版本

    经常由于坑爹的需求,功能要切回到之前的某一个版本.有两种方法可以实现: 方法1: 用svn merge 1) 先 svn up,保证更新到最新的版本,如20: 2) 然后用 svn log ,查看历史 ...

  10. 算法导论学习-RED-BLACK TREE

    1. 红黑树(RED-BLACK TREE)引言: ------------------------------------- 红黑树(RBT)可以说是binary-search tree的非严格的平 ...