无栈协程 | Rust学习笔记

作者:谢敬伟,江湖人称“刀哥”,20年IT老兵,数据通信网络专家,电信网络架构师,目前任Netwarps开发总监。刀哥在操作系统、网络编程、高并发、高吞吐、高可用性等领域有多年的实践经验,并对网络及编程等方面的新技术有浓厚的兴趣。

Rust作为一门新兴语言,主打系统编程。提供了多种编写代码的模式。2019年底正式推出了 async/await语法,标志着Rust也进入了协程时代。下面让我们来看一看。Rust协程和Go协程究竟有什么不同。

有栈协程 vs. 无栈协程

协程的需求来自于C10K问题,这里不做更多探讨。早期解决此类问题的办法是依赖于操作系统提供的I/O复用操作,也就是 epoll/IOCP 多路复用加线程池技术来实现的。本质上这类程序会维护一个复杂的状态机,采用异步的方式编码,消息机制或者是回调函数。很多用 C/C++ 实现的框架都是这个套路,缺点在于这样的代码一般比较复杂,特别是异步编码加状态机的模式对于程序员是一个很大的挑战。但是从另外一个角度看,符合人类逻辑思维的操作方式却恰恰是同步的。

考虑一个web server的场景:每次一个连接一般是请求下载一些数据,如果可以用一个线程来处理每一次新连接,那么这个内部的代码逻辑就可以用同步的方式一路写下来:首先接收数据,然后完成HTTP request解析。根据HTTP头部的信息访问数据库,然后将取得的结果封装在HTTP response中,返回给用户,最后关闭连接。如果是这样,你会发现这里并不需要状态机,也没有什么回调函数,很可能也不需要定时器,整个的过程就是一个流水账,而这正是人类最容易理解的思维方式。然而,我们不能简单地用多线程来解决C10K问题,因为操作系统的线程资源是很有限的,而且是昂贵的。操作系统会限制可以打开的线程数,同时线程之间的切换开销也是比较大的。

Go 有栈协程

Go语言的出现提供了一种新的思路。Go语言的协程则相当于提供了一种很低成本的类似于多线程的执行体。在Go语言中,协程的实现与操作系统多线程非常相似。操作系统一般使用抢占的方式来调度系统中的多线程,而Go语言中,依托于操作系统的多线程,在运行时刻库中实现了一个协作式的调度器。这里的调度真正实现了上下文的切换,简单地说,Go系统调用执行时,调度器可能会保存当前执行协程的上下文到堆栈中。然后将当前协程设置为睡眠,转而执行其他的协程。这里需要注意,所谓的Go系统调用并不是真正的操作系统的系统调用,而是Go运行时刻库提供的对底层操作系统调用的一个封装。举例说明:Socket recv。我们知道这是一个系统调用,Go的运行时刻库也提供了几乎一模一样的调用方式,但这只是建立在 epoll 之上的模拟层,底层的socket是工作在非阻塞的方式,而模拟层提供给我们了看上去是阻塞模式的socket。读写这个模拟的socket会进入调度器,最终导致协程切换。目前Go调度器实现在用户空间,本质上是一种协作式的调度器。这也是为什么如果写了一个死循环在协程里,则协程永远没有机会被换出,一个Processor相当于就被浪费掉了。

有栈的协程和操作系统多线程是很相似的。考虑以下伪代码:

func routine() int
{
    var a = 5
    sleep(1000)
    a += 1
    return a
}

sleep调用时,会发生上下文的切换,当前的执行体被挂起,直到约定的时间再被唤醒。局部变量a 在切换时会被保存在栈中,切换回来后从栈中恢复,从而得以继续运行。所谓有栈就是指执行体本身的栈。每次创建一个协程,需要为它分配栈空间。究竟分配多大的栈的空间是一个技术活。分的多了,浪费,分的少了,可能会溢出。Go在这里实现了一个协程栈扩容的机制,相对比较优雅的解决了这个问题。另外一个问题,关于上下文切换,这一般是跟平台或者CPU相关的代码,因为要涉及到寄存器操作。同时上下文切换也是有一点代价的,因为毕竟需要额外执行一些指令(个人觉得这一点可以忽略掉,无栈的协程实现难道不是也需要一些额外的指令来完成程序逻辑的跳转?)。

有栈协程看起来还是比较直观,特别是对于开发人员比较友好。如果对比一下Rust实现的无栈协程,就会知道因为引入这个栈,保存上下文,从而解决了很多很麻烦的问题。

关于Go,讲一点题外话。

Go有一个比较庞大的运行时刻库。从上文我们了解到,因为Go调度器的需要,运行时刻库把所有的系统调用都做了封装,这些所谓系统调用都被引入了调度器的调度点,也就是说,执行这类系统调用会进行协程的上下文切换。所以换一句话说。Go的系统调用,其实都是被包装过的,能够感知协程的系统调用。所以从这个角度也可以理解为什么Go的运行时刻库是比较庞大的。另外,cgo的执行也是类似的过程。因为调用的C代码非常有可能通过C库来执行系统调用,这样会使线程进入阻塞,从而影响Go的调度器的行为。所以我们看到cgo总会执行entersyscallexitsyscall,就是这个原因。

Rust 协程

绿色线程 GreenThread

早期的Rust支持一个所谓的绿色线程,其实就是有栈协程的实现,与Go协程实现很相似。在0.7之后,绿色线程就被删除了。其中一个原因是,如果引入这样的机制,那么运行时刻库也必须如Go语言一样能够支持有栈协程,也就是之前讨论Go题外话提到的内容。Go没有Native thread的概念,语言层面只支持协程,选择封装全部的系统调用很合理。然而,如果Rust也打算这么做,那么Native thread和协程运行库API统一的问题将很难解决。

无栈协程

无栈协程顾名思义就是不使用栈和上下文切换来执行异步代码逻辑的机制。这里异步代码虽然是异步的,但执行起来看起来是一个同步的过程。从这一点上来看Rust协程与Go协程也没什么两样。举例说明:

async fn routine() 
{
    let mut a = 5;
    sleep(1000).await;
    a = a + 1;
    a
}

几乎是一样的流程。Sleep会导致睡眠,当时间已到,重新返回执行,局部变量a 内容应该还是5。Go协程是有栈的,所以这个局部变量保存在栈中,而Rust是怎么实现的呢?答案就是 Generator 生成的状态机。Generator 和闭包类似,能够捕获变量a,放入一个匿名的结构中,在代码中看起来是局部变量的数据 a,会被放入结构,保存在全局(线程)栈中。另外值得一提的是,Generator 生成了一个状态机以保证代码正确的流程。从sleep.await 返回之后会执行 a=a+1 这行代码。async routine() 会根据内部的 .await 调用生成这样的状态机,驱动代码按照既定的流程去执行。

按照一般的说法。无栈协程有很多好处。首先不用分配栈。因为究竟给协程分配多大的栈是个大问题。特别是在32位的系统下,地址空间是有限的。每个协程都需要专门的栈,很明显会影响到可以创建的协程总数。其次,没有上下文切换,貌似性能也许会好一些?当然,更大的好处是并不需要与CPU体系相关代码,也就有了更好的跨平台的能力。当然,无栈问题也不少。例如,Rust著名的PIN问题。另外,个人觉得Rust的无栈协程主要问题是不那么直观,理解起来会稍微吃力一些。

协程解决的问题

Rust语言真正实现 async/await 语法只是去年底的事情。在那之前,有一些其他临时使用宏的替代做法。所以现在去看一些开源的软件项目,真正采用 await 写代码还是很少的,主要是 poll 的方式,这样的代码需要自己维护各种状态。一个经典的例子就是Sink发送的三件套:poll_ready/start_send/poll_flush,首先需要检查是否缓冲区有待发送的数据,若是,则优先处理这一部分数据。然后检查底层是否就绪,否则无法发送,这时候需要把当前发送的东西转存下来,也就是前面提到的发送缓冲区。如果用C语言写过epoll 相关的代码,那么会发现和这里也没有什么大的区别。因为这就是异步编程大致的模式。而事实上,如果可以用await来写代码,直接调用SinkExt的send().await方法,一切烦恼都消失了。SinkExt::send 内部实现了包含发送缓冲的Sink的三件套,而await 用一种简洁的方式将这一切优雅地呈现出来。这种利用.await 写出来的代码,看似是用同步的方式在做异步的编程,比较简洁,易于理解。

总之,个人觉得Rust异步编程的未来是 await。早期手动来写各种poll方法,实在是太繁琐了。语言实则是一种工具,被发明出来是用来帮助程序员的,而不是造成更多的负担。我相信这也是Rust .await 最大的意义。

下一篇文章,我们来研究下 async/await 究竟做了什么。


深圳星链网科科技有限公司(Netwarps),专注于互联网安全存储领域技术的研发与应用,是先进的安全存储基础设施提供商,主要产品有去中心化文件系统(DFS)、企业联盟链平台(EAC)、区块链操作系统(BOS)。
微信公众号:Netwarps

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349