The `Sync` bound nobody asked for

The Sync bound nobody asked for

没人要求的 Sync 约束

&self on an async trait method whose returned future must be Send implicitly forces Sync on the impl type — even if neither the trait nor its callers ever ask for Sync. 在异步 trait 方法中,如果其返回的 future 必须是 Send 的,那么 &self 会隐式地强制要求实现类型(impl type)必须满足 Sync —— 即使 trait 本身或其调用者从未要求过 Sync

Most async runtimes spawn futures onto a thread pool, which means a spawned future has to be safe to move between threads. tokio::spawn makes the requirement explicit: 大多数异步运行时会将 future 派发(spawn)到线程池中,这意味着被派发的 future 必须能够安全地在线程间移动。tokio::spawn 明确了这一要求:

pub fn spawn<F>(future: F) -> JoinHandle<F::Output> 
where 
    F: Future + Send + 'static, 
    F::Output: Send + 'static,

F: Send cascades through everything the future captures. Whenever a future captures a reference and itself has to be Send, two facts about Rust’s reference types matter: F: Send 的约束会层层传递给 future 所捕获的所有内容。每当 future 捕获一个引用且自身必须是 Send 时,关于 Rust 引用类型的两个事实就变得至关重要:

  • &T: Send requires T: Sync.

  • &mut T: Send only requires T: Send.

  • &T: Send 要求 T: Sync

  • &mut T: Send 仅要求 T: Send

So a Send future that captures &mut T only needs T: Send, but a Send future that captures &T needs T: Sync. In the example below, everything compiles because MyWorker is trivially Send + Sync. 因此,一个捕获了 &mut TSend future 只需要 T: Send,但捕获了 &TSend future 则需要 T: Sync。在下面的示例中,一切都能编译通过,因为 MyWorker 本身就是 Send + Sync 的。

pub trait Worker { fn work(&self) -> impl Future<Output = ()> + 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 + Send + 'static>(w: W) {
    tokio::spawn(async move { loop { w.work().await; } });
}

The Worker trait only visibly asks for Send, so giving the impl type interior mutability seems reasonable. But Cell is Send and !Sync, so it makes MyWorker !Sync too, which breaks the Sync requirement coming from &self. Worker trait 表面上只要求 Send,因此给实现类型加上内部可变性(interior mutability)看起来很合理。但 CellSend 但非 Sync 的,这导致 MyWorker 也变成了非 Sync,从而破坏了由 &self 带来的 Sync 约束。

use std::cell::Cell;
pub trait Worker { fn work(&self) -> impl Future<Output = ()> + 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 + Send + 'static>(w: W) {
    tokio::spawn(async move { loop { w.work().await; } });
}

(Error output omitted for brevity) (此处省略错误输出)

The error walks the chain: fn work captures &self as &MyWorker; for the returned future to satisfy + Send, &MyWorker has to be Send; and &T: Send only holds when T: Sync. The &self parameter has been demanding Sync on Self all along — Cell just made the demand visible. 错误信息揭示了调用链:fn work&self 捕获为 &MyWorker;为了使返回的 future 满足 + Send&MyWorker 必须是 Send 的;而 &T: Send 仅在 T: Sync 时成立。&self 参数一直都在要求 Self 必须是 Sync 的——Cell 只是让这个要求显现了出来。

The cheap fix is to make Self: Sync: swap the non-Sync interior-mutability primitive for a Sync one (a Mutex, an RwLock, an atomic). The impl type becomes Sync and the trait compiles unchanged. But we’ve added synchronisation overhead on every state access for a worker whose state is only ever touched from inside a single spawned task. Suboptimal™. 最简单的修复方法是让 Self: Sync:将非 Sync 的内部可变性原语替换为 Sync 的(如 MutexRwLock 或原子类型)。这样实现类型就变成了 Sync,trait 也能编译通过。但我们为每个状态访问都增加了同步开销,而这个 worker 的状态实际上只会在单个派生任务内部被访问。这显然不是最优解(Suboptimal™)。

The better move is to switch &self to &mut self. &mut T: Send requires only T: Send, no Sync involved: 更好的做法是将 &self 改为 &mut self&mut T: Send 仅要求 T: Send,不涉及 Sync

use std::cell::Cell;
pub trait Worker { fn work(&mut self) -> impl Future<Output = ()> + Send; }
#[allow(dead_code)] struct MyWorker(Cell<()>);
// ... (rest of the implementation)
pub fn spawn<W: Worker + Send + 'static>(mut w: W) {
    tokio::spawn(async move { loop { w.work().await; } });
}

This compiles. Now the trait carries no Sync requirement anywhere. 这样就能编译通过了。现在该 trait 在任何地方都不再带有 Sync 约束。

Underneath all of this is &mut T being the unique reference, not the mutable one. The instinct in Rust is to reach for & over &mut to tighten the contract: no mutation allowed. Here it goes the other way. &mut self guarantees unique access to Self for the duration of the call, which rules out cross-thread sharing and drops the Sync bound with it. 这一切的底层逻辑在于:&mut T 代表的是“唯一引用”(unique reference),而非仅仅是“可变引用”。在 Rust 中,人们倾向于使用 & 而非 &mut 来收紧契约(即禁止修改)。但在这种情况下,逻辑恰恰相反。&mut self 保证了在调用期间对 Self 的唯一访问权,这排除了跨线程共享的可能性,从而去除了 Sync 约束。

相关链接

Full accompanying source code can be found here. Built with rustc 1.95.0. Library versions used: tokio 1.52.2. 完整的配套源代码可以在这里找到。构建环境为 rustc 1.95.0,所用库版本为 tokio 1.52.2