跨await点使用std::sync::Mutex是否必然死锁?Tokio代码阻塞疑问
Tokio 配合 std::sync::Mutex 永久阻塞的原因解析
问题代码
use std::sync::Arc; use std::sync::Mutex; // use tokio::sync::Mutex; use tokio::time::Duration; async fn f(mtx: Arc<Mutex<i32>>, index: usize) { println!("{}: trying to lock...", index); { let mut v = mtx.lock().unwrap(); // let mut v = mtx.lock().await; println!("{}: locked", index); tokio::time::sleep(Duration::from_millis(1)).await; *v += 1; } println!("{}: unlocked", index); } #[tokio::main] async fn main() { let mtx = Arc::new(Mutex::new(0)); tokio::join!(f(mtx.clone(), 1), f(mtx.clone(), 2)); }
输出结果
1: trying to lock... 1: locked 2: trying to lock... (and blocks forever...)
核心疑问
看到相关回答提到阻塞原因是「代码在单线程环境中执行」,但Tokio默认是多线程运行时,await后的任务也能切换到其他工作线程,因此疑惑:是不是运行时恰好把所有任务分配到同一个线程才导致阻塞?这个理解是否正确?
解析
你的理解部分正确,但存在偏差:
std::sync::Mutex的本质问题std::sync::Mutex是同步锁,它的lock()方法是线程级阻塞调用——一旦某个线程调用lock()未获取到锁,操作系统会直接挂起整个线程,直到锁被释放。这种阻塞会彻底占用Tokio的工作线程,破坏Tokio的非阻塞调度模型。不管任务是否在同一线程,都可能触发死锁
- 任务在同一线程:任务1获取锁后,执行到
sleep().await时任务被挂起,但锁仍未释放。任务2被调度到同一线程后,调用lock()会直接阻塞整个线程,导致任务1根本没有机会被唤醒去释放锁,最终死锁。 - 任务在不同线程:任务2所在的线程会被
lock()挂起,而Tokio的工作线程数量有限(默认等于CPU核心数)。如果所有工作线程都被这类阻塞调用占用,持有锁的任务1即使sleep结束,也无法找到可用线程来执行后续的解锁逻辑,同样会导致永久阻塞。
- 正确的解决方案
必须使用Tokio提供的异步锁tokio::sync::Mutex,它的lock().await是异步操作:当无法获取锁时,只会挂起当前任务,而不会阻塞整个线程,线程可以继续处理其他任务。等锁被释放后,Tokio调度器会自动唤醒等待的任务,避免死锁。
总结
代码阻塞的核心原因不是“恰好分配到同一线程”,而是std::sync::Mutex的线程级阻塞特性与Tokio的异步调度逻辑完全不兼容。即使在多线程运行时,只要用错锁类型,就可能触发死锁。
内容的提问来源于stack exchange,提问作者ynn
相关产品推荐
相关产品推荐

