Note.
为什么 Rust 在 1.0 前移除了绿色线程
Note. 为什么 Rust 在 1.0 前移除了绿色线程
在 2014 年底发布 1.0 之前,Rust 曾内置过基于有栈协程的绿色线程(见 RFC 230),后因栈内存管理与零成本抽象的冲突将其移除.
有栈协程要想比操作系统线程更省内存,就不能为每个协程预先分配数兆字节的大栈,而必须采用按需增长的动态栈.动态栈主要有两种实现方案,但都与 Rust 的核心假设冲突:
- 分段栈:将栈组织成由小内存块串成的链表;当函数调用超出当前栈段容量时,临时分配一个新栈段挂上去,返回时再释放. 其主要问题在于热分裂:如果某个处在紧密循环里的函数调用恰好踩在栈段边界上,每次迭代都会触发一次栈段的分配与释放,使普通函数调用的性能变得难以预测.此外,调用 C 语言的外部函数接口(FFI)时,C 代码默认栈空间连续且充足,必须在跨越 FFI 边界时切换回大容量的系统栈.Go(在 1.3 之前)与早期 Rust 都曾使用分段栈,后均因此放弃.
- 栈拷贝:类似
Vec<T>Vec<T>的扩容策略,当栈满时分配一块翻倍的连续内存,将旧栈的字节复制过去,并修正所有指向旧栈的指针. Go 采用这种方案,因为 Go 的逃逸分析与编译器限制了指向栈上变量的指针只能存在于同一条栈内部,且运行时保有类型布局信息(加上垃圾回收器的配合),扫描当前栈即可完成指针重写. 但在 Rust 中,指向栈上局部变量的借用&T&T或&mut T&mut T可以合法地存进堆结构体、传给其他结构,甚至在无运行时反射信息的条件下被编译为裸机器地址.在没有垃圾回收器追踪全部存活指针的前提下,一旦将栈搬迁到新地址,外部持有的栈借用就会立即变成悬垂指针.
放弃分段栈与栈拷贝后,有栈协程就只能退回“预先分配大块连续虚拟内存”的老路,且为了同时兼容原生线程(
libnative
libnative
)与绿色线程(
libgreen
libgreen
),标准库的全部 I/O 与同步原语都不得不通过虚表动态分发.因此,Rust 最终移除了语言内置的绿色线程,转而寻求不依赖独立动态栈的无栈协程方案.