【问题标题】:Recursive locking in GoGo中的递归锁定
【发布时间】:2013-01-18 05:20:01
【问题描述】:

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

【问题讨论】:

标签: go


【解决方案1】:

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

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

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

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

递归(又名可重入)互斥锁是个坏主意。 使用互斥锁的根本原因是互斥锁 保护不变量,也许是内部不变量,例如 “p.Prev.Next == p for all elements of the ring”,或者也许 外部不变量,例如“我的局部变量 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。

递归互斥锁实现将使 G 的 mu.Lock 并且 mu.Unlock 调用在 F 内调用时是无操作的 或当前线程已经拥有 mu 的任何其他上下文。 如果 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 实现递归互斥锁。

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

罗斯

【讨论】:

  • 另一种模式(当需要提供同步时)可能是使用 Go 通道。 Here is an introduction.
  • 您的“证明”引出了一个问题:Erik 有一个问题递归锁定可能会解决,但它在标准库中尚不可用。我们不需要添加支持,因为如果需要,迟早会添加。
  • 这个动机太离谱了。保留不变量不是互斥锁的责任。它的职责是在不变量被破坏时防止相互访问(顾名思义)。您有责任确保在互斥锁解锁时保留不变量,因为在递归互斥锁的情况下,您有责任确保在从一个方法移动到另一个方法时不变量是完整的,因为......它不是互斥责任。
  • 我创建了一个允许计数/递归锁的库。试试看:github.com/jwells131313/goethe
  • 假设一个通用库提供函数func F(callback func()),它锁定一个互斥体,然后调用回调。当然callback是不允许再拨打F的。但如果用户不小心这样做呢?僵局。很难发现,如果其他 go 例程仍在运行。可以使用递归互斥锁轻松检测这种情况,然后改为恐慌(Go 的互斥锁不会这样做),立即使问题对用户显而易见。
【解决方案2】:

您可以很容易地从sync.Mutexsync.Cond 中创建一个递归锁。请参阅Appendix A here 了解一些想法。

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

如果你真的想,当然可以dig the goroutine Id out of the runtime with a bit of C。您可能想阅读该主题,了解为什么 Go 的设计者认为这是一个坏主意。

【讨论】:

    【解决方案3】:

    正如已经确定的那样,从并发的角度来看,这是一个悲惨、可怕、可怕和可怕的想法。

    无论如何,既然您的问题实际上是关于 Go 的类型系统,那么您将如何使用递归方法定义类型。

    type Foo struct{}
    
    func (f Foo) Bar() { fmt.Println("bar") }
    
    type FooChain struct {
        Foo
        child *FooChain
    }
    
    func (f FooChain) Bar() {
        if f.child != nil {
            f.child.Bar()
        }
        f.Foo.Bar()
    }
    
    func main() {
        fmt.Println("no children")
        f := new(FooChain)
        f.Bar()
    
        for i := 0; i < 10; i++ {
            f = &FooChain{Foo{}, f}
        }
        fmt.Println("with children")
        f.Bar()
    }
    

    http://play.golang.org/p/mPBHKpgxnd

    【讨论】:

    • 我想所有支持递归锁的语言(而且是大多数)都是悲惨的、可怕的、可怕的和可怕的。
    • 不一定,在 Go 中管理并发方面很糟糕。您要解决的问题是什么?
    • @ErikAigner 锁通常比其他并发设计结构慢。
    猜你喜欢
    • 1970-01-01
    • 2019-02-06
    • 2010-09-16
    • 2014-09-07
    • 2023-01-12
    • 1970-01-01
    • 1970-01-01
    • 2015-03-21
    • 1970-01-01
    相关资源
    最近更新 更多