没人要求的 `Sync` 约束
摘要
解释了在具有 `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
这是一套动手实践教程系列,旨在弥合理解异步 Rust 内部机制(Future、poll、Pin、执行器)与使用 Tokio 部署实际异步代码之间的差距,面向熟悉 JavaScript 异步和基础 Rust 的开发者。
擦除存在类型
深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。
从头实现异步任务局部变量
本文从头解释如何在 Rust 中实现异步任务局部变量,避免依赖 tokio 生态。它讨论了线程本地存储和执行模型,以实现无需通过函数参数传递变量即可将数据与异步任务关联。
异步编程的承诺与现实
深入剖析异步编程模型的演进——从回调到 Promise——揭示每一轮迭代如何解决先前的资源与性能问题,同时带来新的易用性挑战。
Only Bounds
这篇博客文章介绍了'only bounds',这是对Rust泛型系统的一个提议改进,它用一组更丰富的大小特性代替了当前的Sized/?Sized层次结构,以适应动态大小类型和ARM's Scalable Vector Extension。