Rust对协程的思考
最近和同事聊起来,觉得lua缺乏编译型语言的类型校验功能,还有变量拼写检查之类的,导致线上总是有低级错误出现。比如最近有一个是变量名拼写少了一个字母,导致某功能没开启;还有一个是变量传参时,之前测试多加了一个参数,测试完成后忘记删了,导致参数顺序不对。之前看过有个TypeLua,没想到现在已经不怎么维护了,去搞了Titan-lang,然而Titan也是4个月没动静了。。。
目前也没想到什么好的解决办法,于是顺带看看Rust-lang,开阔一下思路。我最佩服rust这帮人的,是他们有点壮士断腕的勇气,已经写好的runtime和协程,认为和rust的定位不妥,就直接砍掉了。相关的PR在这里,rfcs在这里。rfcs里面关于协程、线程模型的讨论相当到位,我简要翻译一下,权当记录。以下是译文(节译):
背景:线程/任务模型和I/O
很多语言/库会提供任务,任务一般有别于操作系统的原始线程。任务的特性可以按以下几个维度区分:
- 1:1 vs M:N 最根本的问题是一个任务是不是总是对应到一个OS级别的线程(1:1模型),或者是通过用户空间调度器,将任务映射到worker线程上(M:N模型)。有一些内核,比如Windows,支持用户空间调度的1:1模型,结合了这两者的优势。
在M:N模型里,关于任务何时或者是否在worker线程迁移,各自有不同的选择。但是这个模型有个弊端,如果其中一个任务触发了page fault,整个worker线程都会被阻塞。选择合适的worker线程数目是非常困难的,有些框架尝试动态分配线程数,这也会产生额外开销。
- 栈管理。在1:1模型里,任务就是线程,天然具有栈。在M:N模型里,任务可能有他们自己的栈,这里有重要的取舍:
- 分段栈(segmented stacks)允许栈随着时间增长,意味着任务可以有自己的栈,而且依然保持轻量。但是,分段栈有明显的性能问题和复杂度开销。
- 在没有自己的栈的情况下,任务要么无法在工作线程间迁移(例如Java框架里的fork/join),或者只能用CPS(continuation-passing style)实现,即每个阻塞操作都用一个闭包保存自己的工作状态。(CPS一般将用到的栈保存在闭包里)好处是这些任务特别轻量,基本只是闭包的开销。
- 阻塞和I/O支持。在1:1模型里,一个任务可以被任意阻塞而不会影响其他任务,因为每个任务都是一个OS线程。在M:N模型里,OS的阻塞意味着工作线程的阻塞(比如运行较长时间的循环,或者page fault)M:N模型可以用数种方法解决阻塞。在Java的fork/join框架下,是透过动态增减工作线程来实现的。另一种实现,是提供特殊的任务阻塞操作(包括I/O),将阻塞操作化作底层的非阻塞操作,允许底层工作线程继续运行。但是,这种实现只能对显式阻塞起作用,对于循环、page fault之类的就无效了。
Rust的现状
Rust从绿色线程模型(即协程模型)转向了原始线程模型:
- 在Rust的绿色线程模型里,任务是按M:N进行调度,有自己的栈。最初,Rust使用了分段栈,后面改成了预分配的栈,这样Rust的绿色线程就不是轻量级的了。对阻塞的操作下文再叙
- 在Rust的原始线程模型里,任务是1:1的和OS线程匹配的。
(节略)
问题
强制的共同演进:绿色线程模型和原始线程模型必须提供相同的I/O接口,但是有部分接口只会在其中一种模型里有效。比如,轻量级
开销:目前的Rust模型允许运行时将绿色线程模型和原始线程模型混合使用。但是实现上有如下缺点:
- 二进制大小。任意二进制文件里都包含了整个I/O系统的实现,因为他是libstd标准库的一部分。
- 任务局部存储。目前的任务局部存储是可以无缝在原始线程和绿色线程间切换的。但是性能会有影响,即使可以改进,也比直接采用原始的线程局部存储要复杂得多。
- 动态分配和调度。当前的设计下,所有I/O操作都需要动态调度,大部分的内存分配操作也是。但是,绝大部分情况下,
问题重重的I/O交互:
嵌入式的Rust:
维护困难:
posted on 2019-06-02 10:34 lifehacker 阅读(5418) 评论(2) 编辑 收藏 举报