前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我对无栈协程的理解

我对无栈协程的理解

作者头像
ahfuzhang
发布2022-02-22 13:55:03
1.1K0
发布2022-02-22 13:55:03
举报

从后台工程师的角度说,有栈协程的应用更普遍。例如,云风封装的非常经典的基于C的ucontext.h来实现的共享栈的协程,具体请见《C 的 coroutine 库》。而golang在语言级实现的协程是独立栈的协程。

独立栈的协程实现相比共享栈的方式而言少了在每次切换上下文时候的栈数据拷贝,理论上来说性能更高一些,但是也有这样的问题:

  1. 共享栈的栈内存拷贝,也只是拷贝调用方开始的上下文切换的部分,这个数据也不算很大;
  2. 独立栈必然要为每个协程分配栈空间的内存,golang 1.4开始协程栈的大小是2kb,2kb可能对某些协程很浪费,对某些协程又完全不够;协程太多必然也导致分配和GC方面的压力。

之前一直对无栈协程关注不够,认真学一下后,做了如下总结,然后自己写一些代码来模拟无栈协程的运作方式:

无栈协程有这样一些特点:

  1. 无栈协程本质上是generator(生成器),执行generator函数就像是频繁调用某个对象的方法。1.1 上下文保存在这个对象中1.2 对象的方法能够下次进入的时候,继续从上次跳出的地方执行下去
  2. 无栈协程中,所有的generator函数中的局部变量(除非是上下文无关的变量),都要保存在上下文对象中。2.1 因为所有generator函数中的局部变量都在上下对象中,所以上下文对象本身就相当于栈。上下文对象本身可以在堆上分配或者在栈上分配都可以2.2 所有generator函数中的局部变量都在上下对象中,因此这些局部变量相当于对象的成员;对象的成员能够在编译期就确定下来,因此上下文对象的大小在编译期就是确定的2.3 因为上下文对象在编译期就已经知道大小,因此相比有栈协程,肯定不会在切换上下文的时候带来拷贝,也不用担心像有栈协程那样预分配栈空间太大浪费或者太小需要增长的问题
  3. 关于generator函数本身:3.1 函数的第一个参数是上下文对象的指针 3.2 函数内没有任何局部变量,所有的变量访问都从上下文指针中访问对应的成员 3.3 函数入口的第一段代码是从上下文指针中读出上次跳出的代码行的位置(或者某个代表跳出代码行的状态码),然后goto到上次跳出的代码行之后继续执行。这一步是实现继续上次执行结果的关键 3.4 函数执行流程是“主动让出”的,相比操作系统的强制的抢占式的可中断的调度方式而言,generator函数仅仅能够在用户自定义的有限个的跳出点返回。因此,如果在generator函数中存在大量的计算或者调用可能会阻塞的系统函数,则generator之间看起来会并发的假象就会被破坏 3.5 generator函数中不能执行可能会阻塞的函数,例如操作系统IO函数read/write等,调用这些函数之前,要先设置为非阻塞的方式
  4. 调用generator函数的外部代码:4.1 通常在一个循环中调用generator函数4.2 在循环中同时调用多个generator函数,那么这些函数看起来就是并行的——这就是“无栈协程”的运行效果4.3 当这个循环执行结束后,主流程又是“顺序单任务”的状态了。因此,generator能够实现的并发场景是有限的,它并不如有栈协程那么方便

举个栗子

假设有这样一个需求:服务器端的一段业务逻辑,需要同时访问A,B,C三个接口,如果串行访问,那么整个延迟就是ABC三条接口的延迟之和;在ABC三条接口相互不依赖的情况下,我们尝试用无栈协程的方式并发的访问三条接口。

python + gevent的实现

通过gevent能够很好的把python的串行代码修改为并行代码。

代码语言:javascript
复制
 from gevent import monkey; monkey.patch_all()
 import gevent
 import urllib2
 
 def f(url):
     print('GET: %s' % url)
     resp = urllib2.urlopen(url)
     data = resp.read()    #每次做IO的时候都会导致上下文切换,这里相当于调用非阻塞的read(),然后yield
     print('%d bytes received from %s.' % (len(data), url))
 
 def main():
     gevent.joinall([
             gevent.spawn(f, 'https://www.python.org/'),
             gevent.spawn(f, 'https://www.yahoo.com/'),
             gevent.spawn(f, 'https://github.com/'),
     ])
     
 if __name__=="__main__" :
     main()
  • 代码摘录自:https://www.liaoxuefeng.com/wiki/897692888725344/966405998508320

用C实现的伪代码

代码语言:javascript
复制
 struct TcpClientContext{
     int state;          //记录代码执行到哪一行了
     bool is_complete;   //是否已经执行完成
     //以下是业务相关的字段
     const char* addr;
     int fd;
     char send_buffer[MAX_SEND_BUFFER_LEN];
     char send_data_len;
     int send_len;
     char recv_buffer[MAX_RECV_BUFFER_LEN];
     char body_len;
     int recv_len;   
     char header_buffer[MAX_HEADER_LEN];
 }
 
 void main(){
     struct TcpClientContext context1 = {.state=0, .is_complete=false, .addr="192.168.0.11:8080"};
     struct TcpClientContext context1 = {.state=0, .is_complete=false, .addr="192.168.0.12:8080"};
     struct TcpClientContext context1 = {.state=0, .is_complete=false, .addr="192.168.0.13:8080"};
     
     do {
         //以下的三个函数看起来会是并行执行的
         http_fetch(&context1);
         http_fetch(&context2);
         http_fetch(&context3);
     } while(context1.is_complete && context2.is_complete && context3.is_complete);
 }
 
 //模拟generator的伪代码
 void http_fetch(struct TcpClientContext* ctx){
     //函数的第一部分是要决定跳转到哪里
     switch (ctx->state){
     case 0: goto Label_0;
     case 1: goto Label_1;
     case 2: goto Label_2;
     case 3: goto Label_3;
     case 4: goto Label_4;
     case 5: goto Label_5;
     }
 Label_0:
     //连接服务器
     ctx->fd = socket(/*伪代码*/);
     setNonBlocking(ctx->fd);
     connect(ctx->fd, ctx->addr);
     //
     ctx->state = 1;  //进入下一个阶段
     return;
 Label_1:    
     //发送数据阶段
     ctx->send_data_len = make_send_data();
     ctx->send_len = 0;
     do{
         ctx->send_len += write(ctx->fd, ctx->send_buffer+ctx->send_len, ctx->send_data_len-ctx->send_len);
         ctx->state = 2;  //进入下一个阶段
         return;
 Label_2:        
     }while (ctx->send_len<ctx->send_data_len);
     //接收数据阶段
     ctx->recv_len = 0;
     //先读固定字节的header
     do {
         ctx->recv_len += read(ctx->fd, ctx->header_buffer, MAX_HEADER_LEN);
         ctx->state = 3;  //进入下一个阶段
         return;
 Label_3:                    
     } while(ctx->recv_len<MAX_HEADER_LEN);
     ctx->body_len = ntohl(ctx->header_buffer);
     ctx->recv_len = 0;
     do {
         ctx->recv_len += read(ctx->fd, ctx->read_buffer+ctx->recv_len, ctx->body_len - ctx->recv_len);
         ctx->state = 4;  //进入下一个阶段
         return;
 Label_4:
     }while (ctx->recv_len<ctx->body_len);
     
     ctx->is_complete = true;
     close(ctx->fd);
     ctx->fd = 0;
 Label_5:
     ctx->state = 5;  //进入下一个阶段
     return;
 }

看了上面的代码,我们会想:太复杂了,谁会这么写代码?以上只是用C代码来模拟无栈协程的运行模式而已,实际上自带generator(生成器)能力的编程语言会用一些语法糖来屏蔽复杂的切换细节,可以参考python+gevent的实现。

Have Fun,希望你后续能够愉快的使用无栈协程。:-)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一本正经的瞎扯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 无栈协程有这样一些特点:
  • 举个栗子
    • python + gevent的实现
    • 用C实现的伪代码
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档