为什么在将 Tokio 与 std::sync::Mutex 一起使用时会出现死锁?

Kon*_*n W 6 mutex asynchronous rust rust-tokio

我在使用 Tokio 时偶然发现了一个死锁情况:

use tokio::time::{delay_for, Duration};
use std::sync::Mutex;

#[tokio::main]
async fn main() {
    let mtx = Mutex::new(0);

    tokio::join!(work(&mtx), work(&mtx));

    println!("{}", *mtx.lock().unwrap());
}

async fn work(mtx: &Mutex<i32>) {
    println!("lock");
    {
        let mut v = mtx.lock().unwrap();
        println!("locked");
        // slow redis network request
        delay_for(Duration::from_millis(100)).await;
        *v += 1;
    }
    println!("unlock")
}
Run Code Online (Sandbox Code Playgroud)

产生以下输出,然后永远挂起。

lock
locked
lock
Run Code Online (Sandbox Code Playgroud)

根据Tokio docs,使用std::sync::Mutex是可以的:

与普遍的看法相反,在异步代码中使用标准库中的普通互斥体是可以的,而且通常是首选。

但是,用Mutexa替换tokio::sync::Mutex不会触发死锁,并且一切都“按预期”工作,但仅限于上面列出的示例情况。在现实场景中,如果延迟是由某些 Redis 请求引起的,它仍然会失败。

我认为这可能是因为我实际上根本没有生成线程,因此,即使“并行”执行,我也会锁定同一个线程,因为等待只是产生执行。

在不产生单独线程的情况下实现我想要的目标的 Rustacean 方法是什么?

Mat*_*247 6

此处不能使用 a 的原因std::sync::Mutex是您将其握在.await点上。在这种情况下:

  • 任务 1 持有互斥锁,但在 上被挂起delay_for
  • 任务 2 被调度并运行,但无法获取互斥体,因为它仍然属于任务 1。它将在获取互斥体时同步阻塞。

由于任务 2 被阻塞,这也意味着运行时线程被完全阻塞。它实际上无法进入其计时器处理状态(当运行时空闲并且不处理用户任务时发生这种情况),因此无法恢复任务 1。

因此,您现在观察到僵局。

==> 如果您需要在某个点上保持互斥体,.await则必须使用异步互斥体。正如 tokio 文档所述,同步互斥体可以与异步程序一起使用 - 但它们可能无法跨.await点保存。