std::sync::Mutex与tokio::sync::Mutex有什么区别?

17
一个“async”互斥锁与一个“普通”的互斥锁有何不同?我认为这就是tokio的Mutex和普通的std lib Mutex之间的区别。但是,从概念上讲,我不明白互斥锁怎么可能是“async”的。难道不是只有一个东西可以同时使用它吗?
1个回答

28
这里是它们使用的简单比较:
let mtx = std::sync::Mutex::new(0);
let _guard = mtx.lock().unwrap();

let mtx = tokio::sync::Mutex::new(0);
let _guard = mtx.lock().await;

两者都确保互斥性。异步互斥锁和同步互斥锁之间唯一的区别在于它们尝试获取锁时的行为。如果同步互斥锁尝试在已经锁定时获取锁,它将在线程上阻塞执行。如果异步互斥锁尝试在已经锁定时获取锁,它将把执行让给执行器。
如果您的代码是同步的,就没有理由使用异步互斥锁。如上所示,锁定异步互斥锁基于 Future 并且设计用于 async/await 上下文中使用。
如果您的代码是异步的,您可能仍希望使用同步互斥锁,因为开销较小。但是,您应该注意,在 async/await 上下文中阻塞应该尽量避免。因此,只有在不期望阻塞时才使用同步互斥锁。以下是需要记住的一些情况:
- 如果您需要在 .await 调用期间持有锁,请使用异步互斥锁。当使用线程安全的 futures 时,编译器通常会拒绝此操作,因为大多数同步互斥锁不能发送到另一个线程。 - 如果您的锁是有争议的(即,如果您希望在需要时已经锁定互斥锁),则应使用异步互斥锁。这可能发生在将多个任务同步到池或有界队列中时。 - 如果您有复杂和/或计算密集型的更新,那么这些更新可能应该移动到阻塞池中,在那里您将使用同步互斥锁。
上述情况都是同一枚硬币的三个方面:如果您预计会阻塞,请使用异步互斥锁。如果您不知道您的互斥锁用法是否会阻塞,请谨慎使用异步互斥锁。在同步互斥锁足以满足要求的情况下使用异步互斥锁只会留下少量性能,但是在应该使用异步互斥锁的情况下使用同步互斥锁可能是灾难性的。
我遇到的大多数互斥锁情况都是在同步简单数据结构时同步更新方法,其中更新方法已封装为获取锁、更新数据和释放锁。你知道一个简单的 println! 需要锁定一个互斥锁吗?这些互斥锁的用途可以是同步的,即使在异步上下文中也可以使用。即使锁定会阻塞,它通常也不会比进程上下文切换更有影响,而这种切换总是会发生的。
注:Tokio 的 Mutex 具有 .blocking_lock() 方法,如果需要两种锁定行为,则此方法非常有用。因此,互斥锁可以是同步和异步的!
另请参见:

这是一个不错的回答,但当一个已获取异步锁的任务被挂起时会发生什么? - rsalmei
1
@rsalmei 没有什么特别的事情发生;即使在挂起状态下,锁仍然被任务持有。只有当任务恢复以释放它或完全放弃任务时,才能解锁。 - kmdreko
谢谢,@kmdreko,但是这是如何发生的呢?为什么需要异步锁来跨越.await点保持锁定?嗯,我知道物理上获取锁的是操作系统线程,在异步引擎中,每当任务从引擎线程(实际的操作系统线程)中产生时,另一个准备好的线程会取代它。但是当然,这个操作系统线程不应该再获取锁了,所以它必须以某种方式放弃它,不是吗? - rsalmei
锁并不会因为任务被挂起就被释放,那样会失去其作用。你可以在.await跨越同步锁,但许多异步框架默认需要线程安全的任务,并且标准的 Mutex 的锁保护不是线程安全的,所以你经常会遇到问题。如果你想要,可以使用 Mutex ,因为它的保护是线程安全的。 - kmdreko
1
当在.await上持有锁时,你应该考虑使用异步锁的原因并不是由于线程安全问题,而是因为在.await期间任务被挂起(因此锁被持有)的时间是不可预测的(通常是由于I/O)。因此,在异步上下文中的其他地方获取锁时,你知道它可能会被持有很长时间,所以你需要使用异步互斥锁来避免阻塞执行器。如果不这样做,甚至可能会陷入死锁(取决于执行器的工作窃取行为)。 - kmdreko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接