You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

跨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后的任务也能切换到其他工作线程,因此疑惑:是不是运行时恰好把所有任务分配到同一个线程才导致阻塞?这个理解是否正确?

解析

你的理解部分正确,但存在偏差:

  1. std::sync::Mutex的本质问题
    std::sync::Mutex是同步锁,它的lock()方法是线程级阻塞调用——一旦某个线程调用lock()未获取到锁,操作系统会直接挂起整个线程,直到锁被释放。这种阻塞会彻底占用Tokio的工作线程,破坏Tokio的非阻塞调度模型。

  2. 不管任务是否在同一线程,都可能触发死锁

  • 任务在同一线程:任务1获取锁后,执行到sleep().await时任务被挂起,但锁仍未释放。任务2被调度到同一线程后,调用lock()会直接阻塞整个线程,导致任务1根本没有机会被唤醒去释放锁,最终死锁。
  • 任务在不同线程:任务2所在的线程会被lock()挂起,而Tokio的工作线程数量有限(默认等于CPU核心数)。如果所有工作线程都被这类阻塞调用占用,持有锁的任务1即使sleep结束,也无法找到可用线程来执行后续的解锁逻辑,同样会导致永久阻塞。
  1. 正确的解决方案
    必须使用Tokio提供的异步锁tokio::sync::Mutex,它的lock().await是异步操作:当无法获取锁时,只会挂起当前任务,而不会阻塞整个线程,线程可以继续处理其他任务。等锁被释放后,Tokio调度器会自动唤醒等待的任务,避免死锁。

总结

代码阻塞的核心原因不是“恰好分配到同一线程”,而是std::sync::Mutex的线程级阻塞特性与Tokio的异步调度逻辑完全不兼容。即使在多线程运行时,只要用错锁类型,就可能触发死锁。

内容的提问来源于stack exchange,提问作者ynn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 05:53:33