Async Rust: Where does the scheduler live?
Async Rust: Where does the scheduler live?
Async Rust:调度器住在哪里?
The original idea was to kick off this article with a short list of complaints people have about async Rust. Y’know, the type you often see in the wild, justified or otherwise. I then realized that it’d easily be possible to find enough material riffing on async Rust to cover a whole bingo card, and that this would make for a much funnier format. 最初,我打算以一份关于人们对 Async Rust 的抱怨清单作为本文的开篇。你知道的,就是那种你在网上经常看到的,无论是否有理有据。后来我意识到,要找齐足够的素材来填满一张 Async Rust 宾果游戏卡(Bingo card)简直易如反掌,而且这种形式会更有趣。
The next time you see people arguing about async Rust, see if you can get a bingo! A 5×5 bingo card of common async Rust complaints. Let me know if I missed any!! Bonus points for people discussing Zig’s approach to async. Since you are reading this article, you’ve (most likely) already heard some of these. For the others, I assembled a dropdown of suggested reading material, which should get you up to speed. It’s a good way to spend an afternoon, but by no means required reading for this article. 下次当你看到人们争论 Async Rust 时,看看能不能凑成一条宾果!这是一张 5×5 的常见 Async Rust 抱怨宾果卡。如果我漏掉了什么,请告诉我!!如果有人讨论 Zig 的异步处理方式,额外加分。既然你在读这篇文章,你(很可能)已经听说过其中一些了。对于其他的,我整理了一个建议阅读材料的下拉列表,应该能帮你快速上手。这是一个消磨下午时光的好方法,但绝不是阅读本文的必要前提。
Async Rust Bingo Cheat Sheet
Async Rust 宾果作弊表
NOTE: This list is in grid-reading order. 注意: 此列表按网格阅读顺序排列。
Scoped task trilemma: Roughly speaking, you can only pick two out of three: Concurrency, parallelism, borrowing. This is downstream of the leakpocalypse. tl;dr: It’s possible to leak memory, so you cannot guarantee that destructors run. As a result, the original scoped threads API allowed creating data races, very bad. 作用域任务三难困境(Scoped task trilemma): 粗略地说,你只能在并发(Concurrency)、并行(Parallelism)和借用(Borrowing)三者中选其二。这是“内存泄漏危机”(leakpocalypse)的后续影响。简而言之:内存是有可能泄漏的,因此你无法保证析构函数一定会运行。结果就是,最初的作用域线程 API 允许创建数据竞争,这非常糟糕。
Function coloring: Originally the classic Bob Nystrom post. Much ink has been spilled on the topic, see e.g. Without Boats’ own take. There’s so many posts discussing function coloring that I’m not even going to bother to list them, and people regularly disagree on what function coloring even means. 函数着色(Function coloring): 最初源自 Bob Nystrom 的经典文章。关于这个话题已经讨论得太多了,例如可以看看 Without Boats 的观点。讨论函数着色的文章多到我甚至懒得一一列举,而且人们对于“函数着色”到底意味着什么也经常各执一词。
Async Closures: Basically, it took like 6 years to land async closures. The short summary is “async closures need to be lending, since they return futures that may borrow from the closure’s captures”, see this post. You may hear some mumbling about higher-kinded types if you spend too much time looking into the topic, so brace yourself. 异步闭包(Async Closures): 基本上,异步闭包花了大约 6 年时间才落地。简短的总结是“异步闭包需要是借用型的(lending),因为它们返回的 Future 可能会借用闭包捕获的变量”,详见此文。如果你花太多时间研究这个话题,可能会听到一些关于高阶类型(higher-kinded types)的嘀咕,所以做好心理准备。
Tokio Defaultism: The notion of Tokio as the only runtime that matters. Bonus points for people not realizing that what they’re talking about only applies to Tokio, not to async Rust in general. See this Corrode.dev blog article for an overview. Tokio 默认主义(Tokio Defaultism): 认为 Tokio 是唯一重要的运行时的观念。如果有人没意识到他们讨论的内容仅适用于 Tokio,而不适用于整个 Async Rust,额外加分。概览请参阅这篇 Corrode.dev 博客文章。
“just use threads(tm)”: The notion that “almost no one needs async”, and that most people should “just use a lot of threads”. The most common variant of this reasoning is the idea that async only ever becomes necessary if you are writing a server that has to support 10k+ connections at the same time, and that otherwise you can “just use a thread pool”. “直接用线程就行(tm)”: 认为“几乎没人需要异步”,大多数人应该“直接用大量线程”的观点。这种推理最常见的变体是:只有当你编写一个必须同时支持 1 万以上连接的服务器时,异步才是必要的,否则你“直接用线程池就行”。
Complex compiler errors / traces: One of the single most common complaints about async Rust. 复杂的编译器错误/追踪: 关于 Async Rust 最常见的抱怨之一。
Async Drop: Due to the nature of the Rust Drop trait, all destructors are synchronous. The issue is that we may have to perform blocking clean-up actions. In an async context, we want to be able to yield control, so we don’t block any threads. 异步 Drop: 由于 Rust Drop trait 的特性,所有析构函数都是同步的。问题在于我们可能需要执行阻塞式的清理操作。在异步上下文中,我们希望能够让出控制权,以免阻塞任何线程。
Futures are lazy: In JavaScript, any created Future immediately starts executing (they call them Promises over there, but shhh). In Rust, this is not the case! A future is “just” a struct. You have to .await (or spawn) it for it to make any progress, otherwise it doesn’t move at all. Forgetting to do this is a common beginner mistake. Future 是惰性的: 在 JavaScript 中,任何创建的 Future 都会立即开始执行(他们在那里称之为 Promise,嘘)。在 Rust 中并非如此!Future “只是”一个结构体。你必须 .await(或 spawn)它,它才能取得进展,否则它根本不会动。忘记这一点是初学者常犯的错误。
“rust used to have green threads” / stackful coroutines: Green threading is a model in which Rust brings its own runtime. Think of Go’s goroutines. The RFC that removed them can be found here. Rust creator Graydon Hoare’s reflection on his old vision for Rust is also insightful. “Rust 曾经有绿色线程” / 有栈协程: 绿色线程是一种 Rust 自带运行时的模型。想想 Go 的 goroutine。移除它们的 RFC 可以在这里找到。Rust 创建者 Graydon Hoare 对他早期 Rust 愿景的反思也很有见地。
Futures are not runtime agnostic: Task spawning is dependent on the runtime and has various assumptions baked into it. Certain matters are baked into traits. Do it wrong, and you get errors like this. See also, this discussion of writing libraries that can be used across runtimes. In general, when people talk about an “ecosystem split”, this is one of the issues they’re referring to. The other one is that async splits the entire Rust ecosystem into async and non-async libraries. Future 不是运行时无关的: 任务生成(Task spawning)依赖于运行时,并内置了各种假设。某些事项被硬编码在 trait 中。做错了,你就会得到这样的错误。另请参阅关于编写可跨运行时使用的库的讨论。通常,当人们谈论“生态系统分裂”时,指的就是这个问题之一。另一个问题是,异步将整个 Rust 生态系统分裂成了异步库和非异步库。
select! / cancellation safety: See the following excellent transcribed talk, ‘Cancelling Async Rust’. The Tokio select! macro specifically is notorious, which is why its docs have a whole section on cancellation safety. Accidentally dropping futures in select! branches might be the poster child of async Rust footguns. select! / 取消安全性: 请参阅以下精彩的演讲转录稿《取消 Async Rust》。Tokio 的 select! 宏尤其臭名昭著,这就是为什么它的文档中专门有一节关于取消安全性的内容。在 select! 分支中意外丢弃 Future 可能是 Async Rust “坑人”特性的典型代表。
Send + Sync + ‘static bound proliferation: See e.g. the tokio docs on how Send bounds etc. creep into the program. People complain about having to annotate everything with these bounds. Send + Sync + ‘static 约束泛滥: 例如查看 Tokio 文档中关于 Send 约束等是如何蔓延到程序中的。人们抱怨必须用这些约束来标注一切。
lack of ergonomics (free space): No comment. See also, for 2026 goals. 缺乏人体工程学(自由空间): 无评论。另请参阅 2026 年目标。
Futurelock: “a type of deadlock where a resource owned by Future A is required for another Future B to proceed, while the Task responsible for both Futures is no longer polling A.” Please don’t ask me to explain this thing! Futurelock: “一种死锁,即 Future A 拥有的资源是 Future B 继续执行所必需的,而负责这两个 Future 的任务却不再轮询 A 了。” 请不要让我解释这个东西!
effect system / keyword generics / maybe async: See the keyword generics initiative. The idea here is that instead of having to write sync and async functions, you only write a single “maybe async” function. Then, the compiler creates both versions of the function beneath the hood (similar to how generics work) and dispatches the correct one based on calling context. I was always skeptical of this. 效应系统 / 关键字泛型 / maybe async: 请参阅关键字泛型倡议。这里的想法是,与其编写同步和异步函数,不如只编写一个“maybe async”函数。然后,编译器在底层创建该函数的两个版本(类似于泛型的工作方式),并根据调用上下文分派正确的版本。我对此一直持怀疑态度。
APIT, RPIT, TAIT, RPITIT: See here for an explainer. These all refer to the ability to use impl Trait in various places where it was previously not possible. Most of them are easy to look up, but also highly technical. APIT, RPIT, TAIT, RPITIT: 请参阅此处的解释。这些都指在以前无法使用 impl Trait 的地方使用它的能力。它们大多很容易查到,但也非常技术化。
dyn compatibility, Pin<Box<dyn Future<Output = T> + Send>>: The ability to do dynamic dispatch. Due to limitations this may require boxing, which is a bit frustrating, and results in these hilariously verbose types. dyn 兼容性,Pin<Box<dyn Future<Output = T> + Send>>: 进行动态分派的能力。由于限制,这可能需要装箱(boxing),这有点令人沮丧,并导致了这些极其冗长的类型。
io_uring: io_uring is a new (2019) asynchronous I/O interface of the Linux kernel. It has better performance than the older epoll (or at least requires fewer syscalls). In io_uring APIs the kernel owns your buffer until the operation finishes, which fights with futures being cancellable by drop. Fixing it in Tokio requires moving buffers around, instead of passing &mut references to them. io_uring: io_uring 是 Linux 内核的一种新的(2019 年)异步 I/O 接口。它比旧的 epoll 性能更好(或者至少需要的系统调用更少)。在 io_uring API 中,内核拥有你的缓冲区直到操作完成,这与 Future 可以通过 drop 取消的特性相冲突。在 Tokio 中修复这个问题需要移动缓冲区,而不是传递它们的 &mut 引用。