Libs 团队正在研究如何改进 std::sync
模块,可能会将其拆分为新的模块,并在此过程中对 API 进行一些更改。我们正在考虑的 API 更改之一是 Mutex
和 RwLock
的非中毒实现。为了找到最佳前进方向,我们正在进行一项调查,以更清楚地了解标准锁在实际应用中的使用情况。
该调查是一个 Google 表单。 您可以在此处填写。
此调查的目的是什么?
该调查旨在回答以下问题
- 何时故意使用
Mutex
和RwLock
上的中毒。 Mutex
和RwLock
(及其 guard 类型)是否出现在库的公共 API 中。- 从中毒的
Mutex
和RwLock
锁切换到非中毒锁(例如来自antidote
或parking_lot
)有多少摩擦。
这些信息将为 RFC 提供参考,该 RFC 将制定在标准库中实现非中毒锁的路径。它也可能为我们提供一个起点,以研究与恐慌安全相关的 UnwindSafe
和 RefUnwindSafe
特征。
此调查面向哪些人?
如果您编写使用锁的代码,那么此调查适合您。这包括标准库的 Mutex
和 RwLock
,以及来自 crates.io
的锁,例如 antidote
、parking_lot
和 tokio::sync
。
那么中毒到底是什么?
假设您有一个可以更新其余额的 Account
impl Account {
pub fn update_balance(&mut self, change: i32) {
self.balance += change;
self.changes.push(change);
}
}
假设我们还有 balance == changes.sum()
的不变式。我们将其称为余额不变式。因此,在与 Account
交互的任何时候,您都可以始终依赖于其 balance
是其 changes
的总和,这要归功于余额不变式。
在我们的 update_balance
方法中,有一个点没有维护余额不变式
impl Account {
pub fn update_balance(&mut self, change: i32) {
self.balance += change;
// self.balance != self.changes.sum()
self.changes.push(change);
}
}
这看起来没问题,因为我们正处于一个具有对 Account
的独占访问权限的方法中,并且当我们返回时一切都会恢复正常。看不到 Result
或 ?
,因此我们知道在余额不变式恢复之前不会有提前返回的机会。或者我们这样认为。
如果 self.changes.push
没有正常返回怎么办?如果它在没有实际执行任何操作的情况下恐慌了怎么办?那么我们会在没有恢复余额不变式的情况下提前从 update_balance
返回。这似乎也没问题,因为恐慌会开始展开调用它的线程,而不会留下它拥有的任何数据的痕迹。忽略 Drop
特征,没有数据意味着没有被破坏的不变式。问题解决了,对吗?
如果我们的 Account
不由那个恐慌的线程拥有怎么办?如果它作为 Arc<Mutex<Account>>
与其他线程共享怎么办?展开一个线程不会保护仍然可以访问 Account
的其他线程,并且它们不会知道它现在无效。
这就是中毒的用武之地。标准库中的 Mutex
和 RwLock
类型使用一种策略,使恐慌(以及破坏不变式的可能性)可观察。锁的下一个使用者,例如另一个没有展开的线程,可以在那时决定如何处理它。这是通过在锁本身中存储一个开关来完成的,当恐慌导致线程通过其 guard 展开时,该开关会被翻转。一旦该开关被翻转,该锁就被认为是中毒的,并且下次尝试获取它时会收到一个错误而不是一个 guard。
处理中毒锁的标准方法是通过解包它返回的错误来将恐慌传播到当前线程
let mut guard = shared.lock().unwrap();
这样,没有人可以观察到我们共享的 Account
上可能被违反的余额不变式。
听起来不错!那么我们为什么要删除它呢?
锁中毒有什么问题?
中毒本身没有问题。这是一种出色的模式,用于处理可能留下无法工作状态的故障。我们真正要问的问题是它是否应该被标准锁(即 std::sync::Mutex
和 std::sync::RwLock
)使用。我们正在询问是否是标准锁的工作来实施中毒。为了避免任何混淆,我们将中毒模式与标准锁的 API 区分开来,前者称为中毒,后者称为锁中毒。我们只是在谈论锁中毒。
在上一节中,我们提出中毒是一种保护我们免受可能被破坏的不变式影响的方法。锁中毒实际上并不是您想象的那样用于执行此操作的工具。通常,中毒的锁无法判断任何不变式是否实际被破坏。它假设一个锁是共享的,因此很可能比任何可以访问它的单个线程活得更久。它还假设如果恐慌留下任何数据,那么它更有可能处于意外状态,因为恐慌不是 Rust 中正常控制流程的一部分。在恐慌之后一切可能都很好,但是标准锁无法保证。由于无法保证,因此有一个逃生舱口。我们始终可以访问由中毒锁保护的状态
let mut guard = shared.lock().unwrap_or_else(|err| err.into_inner());
所有 Rust 代码都需要在出现恐慌时保持免受任何可能的未定义行为的影响,因此忽略恐慌始终是安全的。Rust 不会尝试保证所有安全代码都没有逻辑错误,因此不会严格认为不潜在导致未定义行为的被破坏的不变式是不安全的。由于忽略锁中毒也始终是安全的,因此它并没有真正为您提供可靠的工具来保护状态免受恐慌的影响。您始终可以忽略它。
因此,锁中毒没有为您提供保证在出现恐慌时安全性的工具。它为您提供的是一种将这些恐慌传播到其他线程的方法。执行此操作所需的机制会增加使用标准锁的成本。必须调用 .lock().unwrap()
会带来人体工程学成本,而必须实际跟踪恐慌状态会带来运行时成本。
使用标准锁,无论您是否需要,都必须支付这些成本。这通常不是标准库中 API 的工作方式。相反,您可以将成本组合在一起,因此您只需为您需要的东西付费。同步访问和传播恐慌是否应该是标准锁的工作?我们不太确定是。如果不是,那么我们应该如何处理呢?这就是调查的用武之地。我们希望更好地了解您在项目中使用锁和中毒的方式,以帮助决定如何处理锁中毒。 您可以在此处填写。