“
所以你会理解,为什么在最近的1百万并发情况下Go使用了那么多的内存。
虽然 Rust 和 Go 都是从前一代编程语言的错误中吸取教训的现代编程语言,但它们在处理并发性的方式上完全不同,这一点正如我们将要看到的,对性能和开发者体验有着巨大的影响。
但首先,为什么我们需要并发性?
如今,大多数程序都需要与可能需要一定时间才能返回响应的资源进行交互:比如网络或磁盘。如果我们在等待网络响应的过程中完全阻塞程序的执行,那将是硬件资源的极其低效利用!
正是因为如此,Go 和 Rust 在语言层面内置了特性,使程序能在某个任务被 I/O(输入/输出)阻塞时执行其他任务。
但是,什么是任务?
任务
任务是可以并发执行的计算抽象单元:多个函数可以被(程序)同时处理,但并不意味着它们会被(CPU)同时执行(真正的并行执行需要多线程)。
在 Go 语言中,你可以使用
go
关键字创建新的任务:
go doSomething()
go doAnotherThing()
而在 Rust 中,你需要使用
spawn
函数:
tokio::spawn(async move {
do_something().await
});
tokio::spawn(async move {
do_another_thing().await
});
在这两种情况下,任务都会被语言的运行时同时处理。但是,什么是运行时?
运行时
运行时的目的是管理和调度不同的任务,以实现硬件的高效利用。
这正是 Rust 和 Go 首次显现出差异的地方。在 Go 中,你无法更改运行时(除非使用完全不同的编译器,如 tinygo),运行时几乎是语言的内置部分;而在 Rust 中,语言本身并不提供运行时,你需要自己引入。
“
在本文中,我们将讨论使用 tokio 运行时的异步 Rust,这是目前最广泛使用且最通用的运行时
当函数在等待某些资源(例如网络)时,会将控制权交还给运行时。在 Go 中,这个过程由标准库、语言和编译器自动完成,而在 Rust 中,则是在遇到
await
关键字时发生。
有栈协程
有栈协程也被称为绿色线程,或 M:N 线程模型(M 个绿色线程运行在 N 个内核线程上),这是 Go 采用的并发模型。在这个模型中,运行时管理轻量级(绿色)线程并将它们调度到可用的硬件线程上。与内核线程类似,每个任务都有自己的栈,并且可以在需要时由运行时动态增长。
有栈协程存在两个主要问题:第一,每个任务都有自己的栈,意味着每个任务都会占用一定的最小内存。截至
Go 1.22
,一个 goroutine 的最小内存占用是 2 KiB,这意味着如果你有 10,000 个并发任务,你的程序将至少使用 20 MiB 的内存。
第二个问题是,运行时需要完全控制栈布局,这使得与其他语言(如 C 的外部函数接口)的互操作变得困难,因为运行时必须在调用 C 代码之前做一些准备栈的工作。这就是为什么 CGO 被认为是缓慢的(实际上,CGO 调用耗时在 30 到 75 纳秒之间,我个人认为这已经相当快了)。
无栈协程
与之相反,Rust 采用了无栈协程方法,任务没有自己的栈。在 Rust 中,Future 基本上是简单的结构体,实现了 Future 特征,每个
.await
链都被编译成巨大的状态机。
异步的问题
如果你在 Python 或 C#中开发,你可能已经了解
async
/
await
的巨大代价:
函数着色问题
[1]
,即同步函数无法调用异步函数,反之亦然。
这导致了许多问题,比如生态系统的分片,使得类库之间难以互操作:因为你使用异步,但某个库不支持异步,所以很难在程序中使用
libA
;此外,还会导致开发者常犯的错误,如阻塞事件循环并降低系统性能。
在 Rust 中,这种情况更加严重,原因有二:首先,标准库没有提供与同步函数对应的异步函数(例如读取整个文件的
read
),其次,不同的运行时甚至无法互操作:如果你开始为
tokio
运行时编写程序,将其移植到另一个运行时将会非常困难。
Go 解决了所有这些问题:所有内容都是同步的,编译器和运行时会自动在调用异步函数时插入
await
点,这对程序员是透明的。但正如我们所看到的,这是以性能为代价的(内存和 CPU 开销)。
一些结语
尽管 Rust 的方法能够最大程度地发挥机器性能,但它带来了一个支离破碎的生态系统,这正给 Rust 的推广造成巨大困难:基础设施不稳定,各方努力缺乏聚焦,每个组织都不得不反复重复造轮子。
想要深入了解并发编程以及运行时的内部工作原理,可以查看我的文章
《异步 Rust:协作式调度 vs 抢占式调度》
[2]
和
《异步 Rust:什么是运行时?Tokio 的内部工作原理》
[3]
。
最后,我推荐阅读文章
《为什么选择异步 Rust?》
[4]
,以了解更多关于异步 Rust 诞生团队的初衷。
原文:
Rust's concurrency model vs Go's concurrency model: stackless vs stackfull coroutines
[1]
函数着色问题:
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function