Go中的递归锁定

Eri*_*ner 20 go

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

zzz*_*zzz 32

很抱歉不直接回答你的问题:

恕我直言,如何在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()
     }
Run Code Online (Sandbox Code Playgroud)

通常,当对mu.Lock的调用返回时,调用代码现在可以假定受保护的不变量保持不变,直到它调用mu.Unlock.

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

递归互斥锁不保护不变量.互斥锁只有一个工作,递归互斥锁不会这样做.

他们有更简单的问题,就像你写的那样

     func F() {
             mu.Lock()
             ... do some stuff
     }
Run Code Online (Sandbox Code Playgroud)

你永远不会在单线程测试中发现错误.但这只是一个更大问题的特殊情况,即它们根本不保证互斥锁要保护的不变量.

如果您需要实现可以使用或不使用互斥锁调用的功能,最明确的做法是编写两个版本.例如,代替上面的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()
     }
Run Code Online (Sandbox Code Playgroud)

或者如果它们都未被导出,那么g和gLocked.

我相信我们最终会需要TryLock; 随时给我们发一个CL.锁定超时似乎不太重要,但如果有一个干净的实现(我不知道一个)那么也许它会没事.请不要发送实现递归互斥锁的CL.

递归互斥体只是一个错误,只不过是一个舒适的错误家园.

拉斯

  • 这种动机是如此.保留不变量不是互斥体的责任.它的职责是在不变量被破坏时防止相互访问(并且名称说明一切).当互斥锁被解锁时,您有责任确保保留不变量,因为在递归互斥锁的情况下,您有责任确保从方法移动到方法时不变量是完整的,因为...它不是互斥责任. (7认同)
  • 假设一个通用库提供函数“func F(callback func())”,它锁定互斥体,然后调用回调。当然`callback`是不允许再次调用`F`的。但如果用户不小心这样做了呢?僵局。如果其他 go 例程仍在运行,则很难发现。递归互斥体可以用来轻松检测这种情况,然后恐慌(Go 的互斥体不会这样做),立即使问题对用户显而易见。 (3认同)
  • 递归互斥体很常见,例如VxWorks、Posix。 (3认同)
  • 你的"证明"引出了一个问题:Erik有一个问题,递归锁定可能会解决,但它在标准库中尚未提供.我们不需要添加支持,因为如果需要它,它迟早会被添加. (2认同)

Nic*_*ood 6

您可以很容易地从sync.Mutexsync.Cond中创建递归锁。请参阅此处的附录 A了解一些想法。

除了Go 运行时不公开任何 goroutine Id 的概念之外。这是为了阻止人们用 goroutine 本地存储做愚蠢的事情,并且可能表明设计者认为如果你需要 goroutine Id,那么你就做错了。

当然,如果你真的愿意的话,你可以用一点 C 语言从运行时中挖掘 goroutine Id 。您可能想阅读该主题,了解为什么 Go 的设计者认为这是一个坏主意。