异步感知原语

如果你浏览tokio的文档,你会发现它提供了很多类型,这些类型“镜像”了标准库中的那些,但加入了异步的特性:锁、通道、计时器等等。

在异步环境下工作时,你应该优先选择这些异步替代品,而不是它们的同步对应物。

为了理解原因,让我们回顾一下上一章中探讨过的互斥锁Mutex

案例研究:Mutex

来看一个简单的例子:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

async fn run(m: Arc<Mutex<Vec<u64>>){
    let guard = m.lock().unwrap();
    http_call(&guard).await;
    println!("Sent {:?} to the server", &guard);
    // `guard`在此处被释放
}

/// 使用`v`作为HTTP请求体
async fn http_call(v: &[u64]){
  // [...]
}
}

std::sync::MutexGuard与放弃点

这段代码能编译,但存在隐患。

我们在异步上下文中尝试获取std中的Mutex锁,然后在.await(在http_call上)的放弃点上保持得到的MutexGuard

假设有两个任务在单线程运行时上并发执行run,我们观察到以下调度事件序列:

      Task A          Task B
         | 
   Acquire lock
 Yields to runtime
         | 
         +--------------+
                        |
              Tries to acquire lock

我们遇到了死锁。任务B永远无法获得锁,因为锁目前被任务A持有,而任务A在释放锁之前已经放弃了运行时的控制权,且运行时无法抢占任务B,因此任务A不会再被调度。

tokio::sync::Mutex

通过切换到tokio::sync::Mutex可以解决这个问题:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use tokio::sync::Mutex;

async fn run(m: Arc<Mutex<Vec<u64>>){
    let guard = m.lock().await;
    http_call(&guard).await;
    println!("Sent {:?} to the server", &guard);
    // `guard`在此处被释放
}
}

现在获取锁是一个异步操作,如果无法继续推进就会让出运行时。回到之前的场景,情况会变成这样:

        Task A          Task B
           | 
   Acquires the lock
   Starts `http_call`
   Yields to runtime
           | 
           +--------------+
                          |
              Tries to acquire the lock
               Cannot acquire the lock
                  Yields to runtime
                          |
           +--------------+
           |
 `http_call` completes      
   Releases the lock
    Yield to runtime
           |
           +--------------+
                          |
                  Acquires the lock
                        [...]

一切顺利!

多线程并不能拯救你

虽然我们在前面的例子中使用了单线程运行时作为执行环境,但在使用多线程运行时时,同样的风险依然存在。 唯一的区别在于导致死锁所需的并发任务数量:在单线程运行时中,2个就足够;而在多线程运行时,我们则需要N+1个任务,其中N是运行时线程的数量。

缺点

拥有异步感知的Mutex伴随着性能上的损失。 如果你确信锁的竞争并不激烈,并且小心地不在放弃点上持有它,你仍然可以在异步上下文中使用std::sync::Mutex

但是,要权衡性能收益与你将承担的活性风险。

其他原语

我们以Mutex为例,但这同样适用于RwLock、信号量等。在异步环境中工作时,应优先选择异步感知版本,以最小化潜在问题的风险。