库团队正在研究如何改进 std::sync
模块,可能会将其拆分为新模块并在此过程中对 API 进行一些更改。我们正在研究的一项 API 更改是非中毒(non-poisoning)的 Mutex
和 RwLock
实现。为了找到最佳的前进方向,我们正在进行一项调查,以更清晰地了解标准锁在实际应用中的使用情况。
本次调查是一个 Google 表单。您可以在此处填写。
本次调查的目的?
本次调查旨在回答以下问题:
- 何时有意使用
Mutex
和RwLock
的中毒特性。 Mutex
和RwLock
(及其守卫类型)是否出现在库的公共 API 中。- 从中毒的
Mutex
和RwLock
切换到非中毒的锁(例如来自antidote
或parking_lot
)有多大的阻力。
这些信息将用于指导一项 RFC,该 RFC 将为标准库中的非中毒锁制定路线。它也可能为我们提供一个起点,来研究与 panic 安全相关的 UnwindSafe
和 RefUnwindSafe
trait。
本次调查的对象?
如果您编写使用锁的代码,那么本次调查就是为您准备的。这包括标准库的 Mutex
和 RwLock
,以及来自 crates.io
的锁,例如 antidote
、parking_lot
和 tokio::sync
。
那么,中毒到底是什么?
假设您有一个可以更新其余额的 Account
:
我们还假设有一个不变式:balance == changes.sum()
。我们称之为*余额不变式*。因此,在与 Account
交互的任何时候,由于余额不变式,您都可以依赖其 balance
等于其 changes
的总和。
然而,在我们的 update_balance
方法中有一个点,余额不变式并未保持:
这似乎没问题,因为我们正在方法执行过程中,拥有对 Account
的独占访问权,并且返回时一切都会恢复正常。看不到 Result
或 ?
,所以我们知道在余额不变式恢复之前没有提前返回的机会。至少我们是这么认为的。
如果 self.changes.push
没有正常返回怎么办?如果它 panic 了,却没有实际执行任何操作怎么办?那么我们就会在没有恢复余额不变式的情况下提前从 update_balance
返回。这似乎也没问题,因为 panic 会开始 unwinding 抛出它的线程,不留下它所拥有的任何数据的痕迹。忽略 Drop
trait,没有数据意味着没有被破坏的不变式。问题解决了,对吧?
如果我们的 Account
不属于那个 panic 的线程怎么办?如果它作为一个 Arc<Mutex<Account>>
与其他线程共享怎么办?Unwinding 一个线程并不能保护仍然可以访问 Account
的其他线程,而它们不会知道它现在是无效的。
这就是中毒出现的地方。标准库中的 Mutex
和 RwLock
类型使用一种策略,使得 panic(以及随之而来的不变式可能被破坏的可能性)变得可观察。锁的下一个消费者,例如一个没有 unwinding 的其他线程,可以在那时决定如何处理它。这是通过在锁本身中存储一个开关来实现的,当 panic 导致线程 unwinding 通过其守卫时,这个开关会被翻转。一旦这个开关被翻转,锁就被认为是*中毒*的,下次尝试获取它时将收到错误而不是守卫。
处理中毒锁的标准方法是通过 unwrap 它返回的错误,将 panic 传播到当前线程:
let mut guard = shared.lock.unwrap;
这样,没有人能够观察到我们共享的 Account
上可能被违反的余额不变式。
这听起来很棒!那为什么我们想要移除它呢?
锁中毒有什么问题?
中毒本身并没有问题。它是处理可能留下不可用状态的失败的绝佳模式。我们真正的问题是它是否应该被*标准锁*使用,即 std::sync::Mutex
和 std::sync::RwLock
。我们正在问实现中毒是否是标准锁的职责。为了避免任何混淆,我们将中毒模式与标准锁的 API 区分开来,前者称为*中毒*(poisoning),后者称为*锁中毒*(lock poisoning)。我们这里只讨论锁中毒。
在前一节中,我们将中毒视为一种保护我们免受可能被破坏的不变式的方法。但锁中毒实际上并非您所想的那样是执行此操作的工具。一般来说,中毒锁无法判断不变式是否*实际*被破坏。它假设锁是共享的,因此很可能比任何可以访问它的单个线程寿命更长。它还假设如果 panic 留下任何数据,那么它更可能处于意外状态,因为 panic 不是 Rust 中正常控制流的一部分。在 panic 之后一切*可能*都还好,但标准锁无法保证这一点。由于没有保证,所以有一个逃生出口。我们总是可以获取中毒锁所保护的状态:
let mut guard = shared.lock.unwrap_or_else;
在 panic 存在的情况下,所有 Rust 代码都需要保持没有可能的未定义行为,因此忽略 panic 始终是安全的。Rust 不会尝试保证所有安全代码都没有逻辑错误,因此不会潜在导致未定义行为的被破坏的不变式并不严格被认为是不安全的。由于忽略锁中毒也始终是安全的,它并不能真正为您提供一个可靠的工具来保护状态免受 panic 的影响。您总是可以忽略它。
所以锁中毒并不能为您提供在 panic 存在的情况下保证安全的工具。它提供的是一种将这些 panic 传播到其他线程的方法。执行此操作所需的机制增加了使用标准锁的成本。在使用 .lock().unwrap()
时存在人体工程学成本,在实际跟踪 panic 状态时存在运行时成本。
使用标准锁,无论您是否需要这些成本,您都必须支付。这与标准库中 API 的工作方式通常不同。相反,您可以将成本组合起来,这样您只需为您所需的功能付费。同步访问*并*传播 panic 是否应该是标准锁的职责?我们对此并不确定。如果不是,那么我们应该如何处理?这就是本次调查的作用所在。我们希望更好地了解您在项目中如何使用锁和中毒,以帮助决定如何处理锁中毒。您可以在此处填写。