在您最意想不到的时候,死锁潜伏在每个角落。我们可以使用 Rust 类型系统在编译时避免它们吗?也就是说,在编译时能证明没有死锁吗?
假设我们有两个互斥体(或互斥体,如果你喜欢的话)M1 和 M2。
如果线程 T1 持有 M1 但希望声明 M2,则会发生死锁;同时线程 T2 拥有 M2 并希望声明 M1。
有几种模式可以避免这种情况:
- 切勿同时申请M1和M2中的一个以上的锁定。
- 永远不要锁定 M2,除非我们已经锁定 M1(反之亦然)
让我们来看看这两种模式。在每种情况下,让我们假设我们正在为Mutex和MutexGuard制作一个新的包装器。
切勿同时申请M1和M2中的一个以上的锁定:
要做到这一点,我们需要确保每个线程正好有一个这样的 "mutex权限令牌 "存在。
在同一个文件中:
thread_local! { |
调用者可以得到MutexPermissionToken--每个线程正好一次。调用者应该期望把它储存在某个地方,因为它将经常需要它。但这并不重要!它是零大小的!- 它是零大小的,所以这只是记账,而不是实际的代码。
我们的新mutex包装器有一个修改过的lock()方法,它接收MutexPermissionToken并将其存储在修改过的MutexGuard中:
impl<T> DeadlockProofMutex<T> { |
MutexGuard需要更多的工作。目前,它在其Drop植入中解锁。我们希望添加一个显式的unlock()方法,检索MutexPermissionToken:
impl<T> DeadlockProofMutexGuard<T> { |
就这样,我们有了它。如果M1和M2都是DeadlockProofMutex类型,编译器会阻止调用者同时索取这两个东西,因此没有死锁。
理论上,由于MutexPermissionToken是零大小的,这些都不会对编译后的代码或运行时活动产生任何影响。
这对调用者来说是否符合人体工程学?好吧,照顾珍贵的MutexPermissionToken可能需要调用者建立一个状态机--但我们知道使用Rust枚举是很省力的,尤其是在编译时防止死锁。
永远不要锁定 M2,除非我们已经锁定 M1(反之亦然)
为了简单起见,我们假设有两种互斥类型--DeadlockProofMutex和DeadlockProofOuterMutex。前者与我们已经描述过的完全一样--它要求你拥有一个MutexPermissionToken,以便能够要求它的锁。
但是这一次,MutexPermissionToken是由外层mutex发出的。(我们删除了之前的MUTEX_PERMISSION_TOKEN)。具体来说,当你锁定它时得到一个,而当你解锁它时返回它。
impl<T> DeadlockProofOuterMutex<T> { |
我们不需要在这里做任何花哨的跟踪。由于互斥的性质,任何时候都只能有一个MutexPermissionToken存在。
只有当外部突变体mutex 被锁定时,任何人都可以尝试索取内部突变体mutex 。防止了死锁。
当然,在实践中可能有比使用一堆互斥体更好的方式来表达你的程序。尽管如此,令我感兴趣的是,Rust 的类型系统可用于解决与内存安全完全无关的棘手错误(死锁)类。这里的一般理论是,其中一些 Rust 设施可以充当子结构类型系统。