future cannot be sent between threads safely
主线位置:任务主线的入口
tokio::spawn。这是类型检查挡在入口的一道门:future 进了任务以后可能换线程,所以必须是Send。
flowchart TB
sp["tokio::spawn"] --> b{"future 是 Send?"}
b -->|否| e[编译错误]
b -->|是| q[进队列]
q --> w1[worker 1 poll]
w1 -->|Pending| s[被别的 worker 偷走]
s --> w2[worker 2 poll]
触发它的代码
跨 .await 持有 std::sync::MutexGuard:
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0u64));
let c = counter.clone();
tokio::spawn(async move {
let mut guard = c.lock().unwrap();
*guard += 1;
tokio::time::sleep(Duration::from_millis(10)).await;
println!("{}", *guard);
})
.await
.unwrap();
}
rustc 1.99.0 的输出:
error: future cannot be sent between threads safely
--> src/bin/ns_guard.rs:8:3
|
8 | / tokio::spawn(async move {
9 | | let mut guard = c.lock().unwrap();
10 | | *guard += 1;
11 | | tokio::time::sleep(Duration::from_millis(10)).await;
12 | | println!("{}", *guard);
13 | | })
| |____^ future created by async block is not `Send`
|
= help: within `{async block@src/bin/ns_guard.rs:8:16: 8:26}`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, u64>`
note: future is not `Send` as this value is used across an await
--> src/bin/ns_guard.rs:11:51
|
9 | let mut guard = c.lock().unwrap();
| --------- has type `std::sync::MutexGuard<'_, u64>` which is not `Send`
10 | *guard += 1;
11 | tokio::time::sleep(Duration::from_millis(10)).await;
| ^^^^^ await occurs here, with `mut guard` maybe used later
note: required by a bound in `tokio::spawn`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.54.1/src/task/spawn.rs:176:21
|
174 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
175 | where
176 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
把 guard 换成 Rc,报错结构一样,只是类型换成 Rc<String>:
= help: within `{async block@src/bin/ns_rc.rs:5:16: 5:21}`, the trait `Send` is not implemented for `Rc<String>`
note: future is not `Send` as this value is used across an await
--> src/bin/ns_rc.rs:7:30
|
6 | let name = Rc::new("job".to_string());
| ---- has type `Rc<String>` which is not `Send`
7 | tokio::task::yield_now().await;
| ^^^^^ await occurs here, with `name` maybe used later
读报错只看两处:has type ... which is not Send 指出是哪个变量,await occurs here 指出它跨过了哪个 .await。
为什么 spawn 要求 Send
约束写在 tokio::spawn 的签名上:F: Future + Send + 'static。
async 块编译成一个状态机,跨 .await 还要用的局部变量存在状态机里。任务在 .await 处返回 Pending,下次被唤醒时不一定在同一个线程上继续:
- 多线程运行时里,空闲的 worker 会从别的 worker 的本地队列偷任务(任务窃取与空闲)。
- 被唤醒的任务可能进全局注入队列,由任意 worker 取走(全局注入队列)。
- 任务调过
block_in_place以后,它的后半段可能已经在另一个线程上(阻塞线程池)。
所以状态机里的所有东西都要能跨线程。MutexGuard 不是 Send,因为有的平台要求在加锁的那个线程上解锁;Rc 的引用计数不是原子的。
current_thread 运行时上 tokio::spawn 也要求 Send:spawn 是同一个函数,编译期不知道运行时是哪种。
怎么修
让 guard 在 .await 之前结束
只需要在锁里做同步的事,就让 guard 的作用域在 .await 之前结束。哪些写法算「结束」,以编译器为准。下面每种写法都放进同一个 tokio::spawn(async move { .. }) 里,在 rustc 1.99.0、edition 2021 下编译:
// 通过:块作用域
{
let mut guard = c.lock().unwrap();
*guard += 1;
} // guard dropped here
tokio::time::sleep(Duration::from_millis(10)).await;
// 通过:语句里的临时值,到 `;` 就销毁
*c.lock().unwrap() += 1;
tokio::time::sleep(Duration::from_millis(10)).await;
// 通过:if 条件里的临时值,进入分支前销毁
if *c.lock().unwrap() == 0 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
// 通过:在同步函数里加锁,返回普通值
let n = bump(&c); // fn bump(c: &Mutex<u64>) -> u64 { let mut g = c.lock().unwrap(); *g += 1; *g }
tokio::time::sleep(Duration::from_millis(n)).await;
// 报错:drop(guard) 之后再 await
let mut guard = c.lock().unwrap();
*guard += 1;
drop(guard);
tokio::time::sleep(Duration::from_millis(10)).await;
// 报错:只在某个分支里 drop
let guard = c.lock().unwrap();
if *guard == 0 {
drop(guard);
}
tokio::time::sleep(Duration::from_millis(10)).await;
// 报错:match 的被匹配表达式里的临时值,活到整个 match 结束
match *c.lock().unwrap() {
0 => tokio::time::sleep(Duration::from_millis(10)).await,
_ => {}
}
// 报错:if let 同理
if let 0 = *c.lock().unwrap() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
drop(guard) 那一种值得单独说:值确实在 .await 之前销毁了,但 rustc 1.99.0 仍然报同样的错误,提示 with mut guard maybe used later。不想靠记规则,就用块或者同步辅助函数,这两种写法的作用域一眼就能看清。
match 和 if let 的坑在于临时值的生命周期:被匹配表达式里产生的 guard 活到整个 match / if let 结束,分支里的 .await 就在它的作用域内。先 let v = *c.lock().unwrap(); 把值取出来,再 match v。
换成 tokio::sync::Mutex
锁里确实要 .await(比如锁住的是一个连接,要在锁里完成一次读写),用 tokio::sync::Mutex,它的 guard 是 Send:
let c = Arc::new(tokio::sync::Mutex::new(0u64));
let c2 = c.clone();
tokio::spawn(async move {
let mut guard = c2.lock().await;
*guard += 1;
tokio::time::sleep(Duration::from_millis(10)).await;
*guard += 1;
})
.await
.unwrap();
println!("tokio Mutex across await: {}", *c.lock().await);
它比 std 的锁贵,什么时候值得用见 tokio::sync::Mutex 还是 std::sync::Mutex。
问: tokio 的 guard 是 Send,那把 Rc 放进 tokio::sync::Mutex,是不是就能跨 .await 了?
答: 不能。Mutex<T> 只有在 T: Send 时才是 Send 和 Sync(mutex.rs 第 258、259 行);guard 里存的是 &Mutex<T>,它要 Send 也得 Mutex<T>: Sync。锁只管「同一时刻一个人访问」,管不了 Rc 的非原子计数:持锁时可以从里面 clone 一个 Rc 带走,锁释放以后,两个线程手里的 Rc 共用一个非原子的计数。把上面例子换成 Arc::new(tokio::sync::Mutex::new(Rc::new(0u64))),rustc 1.99.0 在 move 进 tokio::spawn 时就报错,还没走到 .await:
= help: the trait `Send` is not implemented for `Rc<u64>`
...
has type `Arc<tokio::sync::Mutex<Rc<u64>>>` which is not `Send`
!Send 的状态:spawn_local
Rc、RefCell 或者第三方的 !Send 类型必须跨 .await,就让任务固定在一个线程上,用 LocalSet 加 spawn_local:
let local = tokio::task::LocalSet::new();
local
.run_until(async {
tokio::task::spawn_local(async {
let name = Rc::new("job".to_string());
tokio::task::yield_now().await;
println!("spawn_local with Rc across await: {name}");
})
.await
.unwrap();
})
.await;
上面两段放在一个 #[tokio::main] 里的输出:
tokio Mutex across await: 2
spawn_local with Rc across await: job
spawn_local 的 future 不需要 Send,LocalSet 怎么保证它只在一个线程上被 poll,见 JoinSet、LocalSet、task_local。
LocalSet 也能 spawn_local,是 1.51.0 才稳定的2024 年 8 月的 issue #6739 列了 LocalSet 的几处别扭:LocalSet 里的任务能 tokio::spawn 到运行时上,到了运行时上却没法再 spawn_local 回来;在运行时看来,LocalSet 里的所有任务像是一个任务,event_interval 这类选项会把很多任务算成一个;和 Deno 讨论后发现,LocalSet 比(用 unsafe)直接把 !Send 任务放到 current_thread 运行时上开销大不少。提议是一个本身 !Send 的 LocalRuntime,在它上面 spawn_local 的任务和 tokio::spawn 的任务一样调度。#6808(1.41.0)以 unstable 形式加入了 LocalRuntime,#7557(1.51.0)把它稳定下来。1.54.1 上可以这样写:
#[tokio::main(flavor = "local")]
async fn main() {
let jh = tokio::task::spawn_local(async {
let name = Rc::new("job".to_string());
tokio::task::yield_now().await;
format!("{name}")
});
println!("spawn_local without LocalSet: {}", jh.await.unwrap());
}
spawn_local without LocalSet: job
tokio::spawn 的签名没变,在 LocalRuntime 上照样要求 Send。
spawn_local 也能让 std::sync::MutexGuard 跨 .await 编译通过,但这几乎总是错的:同一个线程上的另一个任务去拿这把锁,会把整个线程阻塞住,持有锁的任务再也没机会恢复。下一页有一个实际死锁的例子。
选哪个
| 情况 | 改法 |
|---|---|
| 锁里只做同步操作 | 块作用域或同步辅助函数,guard 在 .await 前结束 |
锁里要 .await | tokio::sync::Mutex |
Rc / RefCell 等 !Send 值必须跨 .await | 能换就换成 Arc / Mutex;换不了用 LocalSet + spawn_local |
小结
tokio::spawn要求Send,因为任务在.await之间可能换线程:窃取、注入队列、block_in_place都会导致这种情况。- 报错里的
has type ... which is not Send和await occurs here两行指出了是哪个变量、跨过了哪个.await。 - 在 rustc 1.99.0 上,块作用域、语句临时值、
if条件、同步辅助函数都能让 guard 及时结束;drop(guard)、条件 drop、match/if let的被匹配表达式不行。 - 锁里要
.await用tokio::sync::Mutex;!Send状态用spawn_local。