没人要求的 `Sync` 约束

Lobsters Hottest 新闻

摘要

解释了在具有 `Send` future 的异步 trait 方法中使用 `&self` 如何隐式要求实现类型具有 `Sync`,并提供了诸如改用 `&mut self` 或使用 `Sync` 内部可变性等解决方法。

<p><a href="https://lobste.rs/s/c8cv7a/sync_bound_nobody_asked_for">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/08 16:30

# 没人想要的 `Sync` 约束 来源: https://verrchu.github.io/blog/1-the-sync-bound-nobody-asked-for/ 2026年5月4日 ## `&self` 在异步 trait 方法中,当其返回的 future 必须是 `Send` 时,会隐式强制实现类型拥有 `Sync` —— 即使 trait 本身及其调用者从未要求过 `Sync`。 大多数异步运行时将 future 分发到线程池,这意味着被生成的 future 必须能够在线程之间安全移动。 `tokio::spawn` (https://docs.rs/tokio/1.52.2/tokio/task/fn.spawn.html) 将这一要求明确化: ``` pub fn spawn<F>(future: F) -> JoinHandle<F::Output> where F: Future + Send + 'static, F::Output: Send + 'static, ``` `F: Send` 会级联影响到 future 捕获的所有内容。每当一个 future 捕获引用并且自身必须是 `Send` 时,Rust 引用类型的两个事实就变得重要了: - `&T: Send` 要求 `T: Sync`。 - `&mut T: Send` 只要求 `T: Send`。 因此,一个捕获了 `&mut T` 的 `Send` future 只需要 `T: Send`,但一个捕获了 `&T` 的 `Send` future 需要 `T: Sync`。 在下面的例子中,一切都能编译,因为 `MyWorker` 平凡地实现了 `Send + Sync`。 ```rust pub trait Worker { fn work(&self) -> impl Future + Send; } #[allow(dead_code)] struct MyWorker; static_assertions::assert_impl_all!(MyWorker: Send); static_assertions::assert_impl_all!(MyWorker: Sync); impl Worker for MyWorker { async fn work(&self) {} } pub fn spawn<W: Worker>(w: W) { tokio::spawn(async move { loop { w.work().await; } }); } ``` `Worker` trait 只显式要求了 `Send`,因此给实现类型添加内部可变性似乎是合理的。但 `Cell` 是 `Send` 且 `!Sync`,这也会使 `MyWorker` 变成 `!Sync`,从而破坏来自 `&self` 的 `Sync` 要求。 ```rust use std::cell::Cell; pub trait Worker { fn work(&self) -> impl Future + Send; } #[allow(dead_code)] struct MyWorker(Cell<()>); static_assertions::assert_impl_all!(MyWorker: Send); static_assertions::assert_not_impl_any!(MyWorker: Sync); impl Worker for MyWorker { async fn work(&self) {} } pub fn spawn<W: Worker>(w: W) { tokio::spawn(async move { loop { w.work().await; } }); } ``` ``` error: future cannot be sent between threads safely --> examples/step-2.rs:14:25 | 14 | async fn work(&self) {} | ^^^^^ future returned by `work` is not `Send` | = help: within `MyWorker`, the trait `Sync` is not implemented for `Cell<()>` = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` note: captured value is not `Send` because `&` references cannot be sent unless their referent is `Sync` --> examples/step-2.rs:14:19 | 14 | async fn work(&self) {} | ^^^^^ has type `&MyWorker` which is not `Send`, because `MyWorker` is not `Sync` note: required by a bound in `Worker::work::{anon_assoc#0}` --> examples/step-2.rs:4:50 | 4 | fn work(&self) -> impl Future + Send; | ^^^^ required by this bound in `Worker::work::{anon_assoc#0}` ``` 错误信息清晰地展示了链条:`fn work` 捕获了 `&self` 作为 `&MyWorker`;为了让返回的 future 满足 `+ Send`,`&MyWorker` 必须是 `Send`;而 `&T: Send` 只有在 `T: Sync` 时才成立。`&self` 参数一直在暗中要求 `Self: Sync` —— `Cell` 只是让这个要求变得可见了。 ## 廉价的修复是让 `Self: Sync` 将非 `Sync` 的内部可变性原语替换成 `Sync` 的(Mutex、RwLock、原子类型)。实现类型变成 `Sync`,trait 保持不变即可编译。 但这样做会为每次状态访问增加同步开销,而 worker 的状态只会在单个生成的任务内部被访问。次优TM。 ## 更好的做法是将 `&self` 改为 `&mut self` `&mut T: Send` 只要求 `T: Send`,不涉及 `Sync`: ```rust use std::cell::Cell; pub trait Worker { fn work(&mut self) -> impl Future + Send; } #[allow(dead_code)] struct MyWorker(Cell<()>); static_assertions::assert_impl_all!(MyWorker: Send); static_assertions::assert_not_impl_any!(MyWorker: Sync); impl Worker for MyWorker { async fn work(&mut self) {} } pub fn spawn<W: Worker>(mut w: W) { tokio::spawn(async move { loop { w.work().await; } }); } ``` 这段代码可以编译。现在 trait 没有任何地方要求 `Sync`。 这一切背后的原理是:`&mut T` 是唯一引用,而非可变引用。在 Rust 中,我们本能地倾向于使用 `&` 而非 `&mut` 来收紧约定:不允许可变。但在这里恰恰相反。`&mut self` 保证了在调用期间对 `Self` 的唯一访问,从而排除了跨线程共享,并随之去掉了 `Sync` 约束。 ## 链接 - Niko Matsakis, *聚焦所有权* (https://smallcultfollowing.com/babysteps/blog/2014/05/13/focusing-on-ownership/) —— 关于 `&mut` 作为唯一性而非可变性的经典文章。 --- *完整配套源代码可以在[此处](https://github.com/verrchu/blog/tree/main/content/1-the-sync-bound-nobody-asked-for)找到。使用 `rustc 1.95.0` 构建。使用的库版本:`tokio 1.52.2`。*

相似文章

谁在运行你的 Rust Future?动手实践入门异步 Rust

Hacker News Top

这是一套动手实践教程系列,旨在弥合理解异步 Rust 内部机制(Future、poll、Pin、执行器)与使用 Tokio 部署实际异步代码之间的差距,面向熟悉 JavaScript 异步和基础 Rust 的开发者。

擦除存在类型

Lobsters Hottest

深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。

从头实现异步任务局部变量

Lobsters Hottest

本文从头解释如何在 Rust 中实现异步任务局部变量,避免依赖 tokio 生态。它讨论了线程本地存储和执行模型,以实现无需通过函数参数传递变量即可将数据与异步任务关联。

异步编程的承诺与现实

Hacker News Top

深入剖析异步编程模型的演进——从回调到 Promise——揭示每一轮迭代如何解决先前的资源与性能问题,同时带来新的易用性挑战。

Only Bounds

Lobsters Hottest

这篇博客文章介绍了'only bounds',这是对Rust泛型系统的一个提议改进,它用一组更丰富的大小特性代替了当前的Sized/?Sized层次结构,以适应动态大小类型和ARM's Scalable Vector Extension。