一尘不染

Go中的递归锁定

go

Go的sync包裹中有个Mutex。不幸的是,它不是递归的。在Go中实现递归锁的最佳方法是什么?


阅读 344

收藏
2020-07-02

共1个答案

一尘不染

很抱歉没有直接回答您的问题:

恕我直言,在Go中实现递归锁的最好方法是不实现它们,而是重新设计代码以一开始就不需要它们。我认为,对他们的渴望可能表明对某些(此处未知)问题使用了错误的方法。

作为上述权利要求的间接“证明”:递归锁是否是对涉及互斥锁的某些常见情况的一种常见/正确方法,它迟早会包含在标准库中。

最后,最后但并非最不重要的一点:Go开发团队的Russ Cox在这里https://groups.google.com/d/msg/golang-
nuts/XqW1qcuZgKg/Ui3nQkeLV80J写道:

递归(又称可重入)互斥是一个坏主意。使用互斥锁的根本原因是互斥锁保护不变式,也许是内部不变式,例如“ p.Prev.Next ==
p代表环的所有元素”,或者是外部不变式,例如“我的局部变量x等于p.Prev ”。

锁定互斥锁会断言“我需要保持不变式”,并且也许“我会暂时破坏这些不变式”。释放互斥量会断言“我不再依赖那些不变式”和“如果我打破了它们,我就将它们还原了”。

了解互斥体保护不变式对于确定需要互斥体和不需要互斥体至关重要。例如,用原子增量和减量指令更新的共享计数器是否需要互斥锁?这取决于不变性。如果唯一不变的是在i递增和d递减后计数器具有值i-d,则指令的气氛确保不变。不需要互斥。但是,如果计数器必须与其他数据结构同步(也许它对列表中元素的数量进行计数),则单个操作的原子性是不够的。通常是互斥体的其他东西必须保护更高级别的不变式。这就是不能保证Go中的地图操作是原子的原因:在典型情况下,这样做会增加费用而没有收益。

让我们看一下递归互斥体。假设我们有如下代码:

     func F() {
             mu.Lock()
             ... do some stuff ...
             G()
             ... do some more stuff ...
             mu.Unlock()
     }

     func G() {
             mu.Lock()
             ... do some stuff ...
             mu.Unlock()
     }

通常,当返回对mu.Lock的调用时,调用代码现在可以假定受保护的不变式成立,直到调用mu.Unlock为止。

从F或当前线程已经拥有mu的任何其他上下文中调用时,递归互斥体实现将使G的mu.Lock和mu.Unlock调用成为no-
ops。如果mu使用了这样的实现,则当mu.Lock返回G内部时,不变量可能成立,也可能不成立。这取决于F在调用G之前所做的事情。也许F甚至没有意识到G需要这些不变式并且已经破坏了它们(完全有可能,尤其是在复杂代码中)。

递归互斥不能保护不变式。互斥锁只有一项工作,而递归互斥锁则不行。

它们有更简单的问题,例如您写的

     func F() {
             mu.Lock()
             ... do some stuff
     }

您将不会在单线程测试中找到该错误。但这只是更大问题的特例,那就是它们根本无法保证互斥体要保护的不变性。

如果您需要实现可以在持有或不持有互斥锁的情况下进行调用的功能,那么最清晰的事情就是编写两个版本。例如,代替上面的G,您可以编写:

     // To be called with mu already held.
     // Caller must be careful to ensure that ...
     func g() {
             ... do some stuff ...
     }

     func G() {
             mu.Lock()
             g()
             mu.Unlock()
     }

或如果它们都未导出,则为g和gLocked。

我确信我们最终将需要TryLock;请随时向我们发送一份CL。用超时锁定似乎不是很重要,但是如果有一个干净的实现(我不知道一个实现),那可能就可以了。请不要发送实现递归互斥的CL。

递归互斥体只是一个错误,无非就是一个舒适的错误之家。

拉斯

2020-07-02