今天看了下云风写的关于 c coroutine博客 (代码), 发现 coroutine 实现原理其实还比较简单,就用户态栈切换,只需要几十行汇编,特别轻量级。

具体实现

1. 创建一个coroutine: 也就是创建一块连续内存,用于存放栈空间,并设置好入口函数所需要的寄存器

  makecontext glibc c语言实现

2. resume coroutine:  push保存当前执行上下文的寄存器到栈上,修改%rsp寄存器, jmp 到指定coroutine 执行指令位置,pop 恢复寄存器,开始执行

swapcontext glibc 汇编实现

3. yield coroutine: 同resume

栈切换涉及寄存器操作,得用汇编实现, x86 8个通用寄存器,x64 16个,通过push 保存到栈,pop 恢复到寄存器;比较重要寄存器%rsp 栈顶指针,%rip 指令指针不能直接操作,通过call、jmp 跳转新的Code执行位置。

在64汇编中,并不需要对16个寄存器都备份,其中%rax作为返回值、%r10 %r11 被调用方使用前会自己备份.

参考: X86-64寄存器和栈帧

X86-64寄存器的变化,不仅体现在位数上,更加体现在寄存器数量上。新增加寄存器%r8到%r15。加上x86的原有8个,一共16个寄存器。

刚刚说到,寄存器集成在CPU上,存取速度比存储器快好几个数量级,寄存器多了,GCC就可以更多的使用寄存器,替换之前的存储器堆栈使用,从而大大提升性能。


让寄存器为己所用,就得了解它们的用途,这些用途都涉及函数调用,X86-64有16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。其中:

  1. %rax 作为函数返回值使用。
  2. %rsp 栈指针寄存器,指向栈顶
  3. %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
  4. %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
  5. %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值

 

cloudwu/coroutine 测试

测试环境:R620 E5-2620 2.4G

测试次数:1kw 次yeild操作

结果:

time ./main

real 0m7.886s
user 0m4.408s
sys 0m3.447s

分析:

- 单核心每秒1.27M/s yield,每次耗时约 2000 cpu周期
- 因sys占用近一半时间,strace统计 每次yield至少两次 rt_sigprocmask 系统调用,glibc 还考虑到了sig 设置的切换,其实必要性不大
- 切换时栈需要memcpy,栈因为需要预先分配,一般都在1M左右,但实际使用很少超过10K,如果为每个coroutine 预先分配1M,内存消耗过大。
  云风实现里面,只分配一个1M的栈,coroutine 切换时才将实际大小的栈memcpy出来。节省内存,但性能消耗也不可忽视
- 如果去掉syscall,性能会有很大提升
 

微信libco协程库

在infoq一个关于微信后端存储视频提到: https://github.com/starjiang/libco
相比:
- 没有使用glibc,只支持linux,但总体和glibc 实现类似,优化掉了 rt_sigprocmask
- 为每个coroutine 预分配128K的栈
- 包含 epoll 网络库实现
- 单线程版本
- 超时控制
 
就看看socket 的read 实现:
ssize_t read( int fd, void *buf, size_t nbyte )
{
.......
int timeout = ( lp->read_timeout.tv_sec * )
+ ( lp->read_timeout.tv_usec / ); struct pollfd pf = { };
pf.fd = fd;
pf.events = ( POLLIN | POLLERR | POLLHUP ); int pollret = poll( &pf,,timeout ); // 此处上下文将yeild, 切换到到epoll_wait 直到fd可读,当前协程才会被重新resume ssize_t readret = g_sys_read_func( fd,(char*)buf ,nbyte ); // read系统调用 if( readret < )
{
co_log_err("CO_ERR: read fd %d ret %ld errno %d poll ret %d timeout %d",
} return readret; } int poll(struct pollfd fds[], nfds_t nfds, int timeout)
{
......
return co_poll( co_get_epoll_ct(),fds,nfds,timeout );
} int co_poll( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout )
{
....
for(nfds_t i=;i<nfds;i++)
{
arg.pPollItems[i].pSelf = fds + i;
arg.pPollItems[i].pPoll = &arg; arg.pPollItems[i].pfnPrepare = OnPollPreparePfn;
struct epoll_event &ev = arg.pPollItems[i].stEvent; if( fds[i].fd > - )
{
ev.data.ptr = arg.pPollItems + i;
ev.events = PollEvent2Epoll( fds[i].events ); epoll_ctl( epfd,EPOLL_CTL_ADD, fds[i].fd, &ev ); // 添加epoll监听事件
}
//if fail,the timeout would work } co_yield_env( co_get_curr_thread_env() ); // yiled 协程,将被切换到epoll_wait
....
} void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{
epoll_event *result = (epoll_event*)calloc(, sizeof(epoll_event) * stCoEpoll_t::_EPOLL_SIZE ); for(;;)
{
int ret = epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, );
// 超时时间1ms,而不是一直等待,方便做send timeout 处理
....
// resume 收到数据fd所在coroutine
}

总体上,让accept、read、write 等操作网络IO操作,用同步方式来写但实际以NIO方式执行,不阻塞线程,减少代码量同时逻辑更清晰。

 
 

其他语言

Java: 以前公司rpc有通过JavaFlow实现,但没有正式用,貌似有性能和其他一些问题;虚机语言线程上下文较为复杂,不像c那么简单切换栈。

C#: IEnumerator不怎么完善版本后,4.5 使用语法糖 await 编译器技巧实现类似效果。

Erlang: 原生进程模型就是coroutine,相比上面实现就是玩具;多线程、跨线程任务迁移、私有堆和栈、线程相关内存分配器、消息箱、公平调度等。Erlang 进程栈因为解释执行,栈空间不是由CPU自动管理,不需要连续的,可以动态扩展,没上限可递归到OOM。

Golang:goroutine Erlang的低配版,够用也实用;可惜没有分布式支持,但关键golang执行性能比Erlang高至少一个数量级。

 
图片来自coursera 北京大学的 计算机组成课程

c coroutine的更多相关文章

  1. Coroutine in Java - Quasar Fiber实现--转载

    转自 https://segmentfault.com/a/1190000006079389?from=groupmessage&isappinstalled=0 简介 说到协程(Corout ...

  2. The Coroutine

    关于Coroutine 说到coroutine就不的不说subroutine,也就是我们常用到的一般函数.调用一个函数开始执行,然后函数执行完成后就退出,再次调用的时候,再从头开始,调用之间是没有保存 ...

  3. lua coroutine for iterator

    背景 前面的文章演示了使用闭包函数实现 状态的迭代器. 本文演示使用 coroutine来产生迭代器的例子. coroutine迭代器例子 -- 遍历二叉树 local binary_tree = { ...

  4. python中的generator(coroutine)浅析和应用

    背景知识: 在Python中一个function要运行起来,它在python VM中需要三个东西. PyCodeObject,这个保存了函数的代码 PyFunctionObject,这个代表一个虚拟机 ...

  5. hive源码之新建一个coroutine

    最近由于项目需要读了一下云风老大的hive项目代码,因为对lua只有熟悉的水平,下面的东西必然多多错误:),只为记录. lua_State *sL = schedule_newtask(L); str ...

  6. Lua Coroutine详解

    协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈,局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西.线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程 ...

  7. 【Unity3D基础教程】给初学者看的Unity教程(五):详解Unity3D中的协程(Coroutine)

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 为什么需要协程 在游戏中有许多过程(Proc ...

  8. Lua 协程coroutine

    协程和一般多线程的区别是,一般多线程由系统决定该哪个线程执行,是抢占式的,而协程是由每个线程自己决定自己什么时候不执行,并把执行权主动交给下一个线程. 协程是用户空间线程,操作系统其存在一无所知,所以 ...

  9. U3D中的 Coroutine程序 解析

    今天咱就说说协同程序coroutine. 什么是协同程序 先说说啥是协程:它的表现形式非常像线程,对线程有过接触的朋友可能更理解我这句话的意思,你没接触过线程,那么理解它会有一些难度.但是它不存在线程 ...

随机推荐

  1. Android开发之画图的实现

    Android开发之画图的实现    四天前上完安卓的第一节课,真的是一脸懵逼,尽管熊哥说和java是差不多的,然而这个包和那个包之间的那些转换都是些什么鬼呀!!!但是四天的学习和操作下来,我觉得安卓 ...

  2. Python小白的发展之路之Python基础(二)

    列表.元组操作 字符串操作 字典操作 集合操作 文件操作 字符编码与转码 1.列表.元组操作 (1)列表 列表是可变的(mutable)--可以改变列表的内容,这不同于字符串和元组,字符串和元组都是不 ...

  3. jQuery之回调对象

    1. jQuery 1.7 版本中新增的 jQuery.Callbacks() 函数返回一个全能的对象,此对象对管理回调列表提供了强大的方式.它能够增加.删除.触发.禁用回调函数. 2. callba ...

  4. Appium环境搭建+cordova

    1.安装JDK 配置JAVA_HOME(变量值为jdk的安装目录)以及Path path值如下: 验证是否生效 2.安装node.js 选择适合自己的版本官网直接下载https://nodejs.or ...

  5. linux0.11改进之四 基于内核栈的进程切换

    这是学习哈工大李治军在mooc课操作系统时做的实验记录.原实验报告在实验楼上.现转移到这里.备以后整理之用. 完整的实验代码见:实验楼代码 一.tss方式的进程切换 Linux0.11中默认使用的是硬 ...

  6. 【poj2154】 Color

    http://poj.org/problem?id=2154 (题目链接) 题意 n个珠子的项链,可以染上n中颜色,项链可以旋转不能翻转,求染色方案数. Solution 经典的公式: \begin{ ...

  7. css使absolute相对于父容器进行定位而不是以body(为什么绝对定位(absolute)的父级元素必须是相对定位(relative))

    借知乎的回答如下解释: 首先,我想告诉你的是,如果父级元素是绝对定位(absolute)或者没有设置,里面的绝对定位(absolute)自动以body定位.这句话是错的.正确的是:只要父级元素设了po ...

  8. Python报错UnicodeDecodeError: ascii codec can t decode byte 0xe0 ...解决方法

    用命令(python setup.py install)安装webpy时候总是报错 在网上搜索到的解决方法如下: 1. 这是Python 2 mimetypes的bug 2. 需要将Python2.7 ...

  9. 为什么FFT时域补0后,经FFT变换就是频域进行内插?

    应该这样来理解这个问题: 补0后的DFT(FFT是DFT的快速算法),实际上公式并没变,变化的只是频域项(如:补0前FFT计算得到的是m*2*pi/M处的频域值, 而补0后得到的是n*2*pi/N处的 ...

  10. W3Help-兼容性-知识库

    http://www.w3help.org/zh-cn/kb/ clear:none/left/right/both/inherit该特性表明元素框的哪一边不可以和先前的浮动框相邻.'clear' 特 ...