一句话总结
多线程运行时会把任务在线程间搬运,所以 tokio::spawn 的 Future 必须是 Send + 'static.
最常见的报错是:跨一个 .await 持有了非 Send 的值(如 Rc 或 std::MutexGuard),编译器据此判定整个任务非 Send.理解了状态机,这个报错就不再神秘.
为什么偏偏在异步里撞上 Send
Send,Sync,'static 在多线程章节已经见过:Send 表示一个值可以安全地移动到另一个线程.线程编程里它出现得理所当然--确实把数据交给了新线程.
异步里它的出现一度让人意外:"没开线程,怎么也要 Send?"答案藏在上一篇的伏笔里--Tokio 默认的多线程调度器,会用工作窃取把任务从一个线程挪到另一个线程执行.任务在线程间移动,自然要求任务本身可跨线程移动,即 Send.
|
spawn 的完整约束
看 tokio::spawn 的签名(简化),约束写得明明白白:
|
| 约束 | 含义 | 为什么需要 |
|---|---|---|
Send |
任务可被移动到别的线程 | 多线程调度器会跨线程搬运任务 |
'static |
任务不借用任何短命的外部数据 | 任务存活时长由运行时决定,可能比调用栈活得久 |
常见误解.'static 在这里的意思是"不含任何非 'static 的借用"--要么是拥有所有权的值,要么借用的是 'static 数据.
因为 spawn 出去的任务可能在当前函数返回后仍在运行,它不能持有指向当前栈帧的引用.所以 spawn 的闭包几乎总要 move,把数据所有权搬进任务.
核心规则:跨 await 的值决定任务是否 Send
这是本篇最该记住的一句话.回顾第 04 篇:跨 .await 存活的局部变量,会被编译器存进 Future 状态机结构体.因此:
任务这个状态机是否 Send,取决于它"存档"里的所有字段是否都 Send.
只要有一个跨 .await 存活的变量不是 Send,整个状态机就不是 Send,于是不能 spawn.
就像一个集装箱能否上船,取决于箱内每一件货物是否都允许海运--一件违禁品,整箱都走不了.
关键限定词是"跨 await 存活".如果一个非 Send 的值在 .await 之前就用完,被丢弃,它不会进入状态机,也就不影响任务的 Send 性.
典型报错一:跨 await 持有 Rc
Rc(非线程安全的引用计数)是经典的非 Send 类型.下面的代码无法 spawn:
|
因为 data 在 .await 两侧都被使用,它被存进状态机,而 Rc 非 Send,任务整体非 Send.两种修法:
|
典型报错二:跨 await 持有 MutexGuard
更隐蔽,也更高频的一个:持有标准库 std::sync::Mutex 的锁守卫(MutexGuard)跨越 .await.
|
这个报错其实在保护我们:持有锁的同时 .await 让出,锁会一直被占着,直到任务被重新调度--期间其他任务全部卡在抢这把锁上,极易造成性能塌方甚至死锁.编译器用 Send 约束逼我们避开它.正确做法是在 await 前释放锁:
|
确有此类需求(例如要在持锁期间做异步操作).这时应改用 Tokio 提供的异步锁 tokio::sync::Mutex,它的守卫是 Send 的,设计上允许跨 await 持有.
但它更慢,不能无脑替换 std Mutex.何时用哪个,正是下一篇的主题.
读懂这类报错的套路
遇到 "future cannot be sent between threads safely" 时,按这个套路排查:
|
和 Go 的对比
Go 里把 sync.Mutex 锁着跨过一次"阻塞调用"(如 channel 接收),编译器毫不阻拦--出了死锁要靠运行时排查,靠经验.
Rust 把这类并发危险提前到编译期拦下.报错初看烦人,实则替我们挡掉了一类最难调试的线上事故.
快速回顾
- 根源:多线程调度器跨线程搬运任务,故
spawn要求 Future 满足Send + 'static. - 'static 的真意:不持有非 'static 借用,通常靠
move把所有权搬进任务,而非"活到程序结束". - 核心规则:任务是否 Send,取决于跨 await 存活的所有变量是否都 Send;await 前用完的值不影响.
- 两大典型:跨 await 持有
Rc,持有std::MutexGuard→ 任务非 Send. - 修复:用内层作用域让其在 await 前释放,或换 Send 等价物(
Arc,tokio::Mutex). - 价值:这是编译器在编译期替我们拦截"持锁过 await"等高危并发 bug.
动手练习
- 复现报错:在
tokio::spawn的任务里跨.await持有一个Rc,复现报错并读懂编译器指认的类型. - 两种修复:用"内层作用域"和"换 Arc"两种方式分别修复上题.
- MutexGuard 报错:写一个跨
.await持有std::sync::MutexGuard的函数,观察报错,再改成 await 前释放锁. - 解释 'static:解释
'static在 spawn 约束里的真实含义,并说明为什么 spawn 闭包常需move. - 一句话概括:为什么"任务的 Send 性"等于"其状态机所有字段的 Send 性"?