Rust in Everything
基于 tokio 1.54.1

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。

📝NOTE
没有蠢问题

问: 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。

📝NOTE
冷知识:不用 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 前结束
锁里要 .awaittokio::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。

💬 关联讨论

发起讨论
加载中...