【问题标题】:How can this pattern result in a deadlock?这种模式如何导致死锁?
【发布时间】:2017-12-31 13:39:03
【问题描述】:

我有一个 (LRU) 缓存对象,但遇到了死锁……这怎么可能?

  type cache struct {
    mutex *sync.Mutex
    ...
  }

  func (this *cache) Init() {  // guaranteed to be called once, in main()
    this.mutex = &sync.Mutex{}
  }

  func (this *cache) f1() {
     // Pattern for accessing mute, at the top of any function of 'cache' where needed.
     this.mutex.Lock()
     defer this.mutex.Unlock()
     ...
  }


  func (this *cache) f2() {
     this.mutex.Lock()
     defer this.mutex.Unlock()
     ...
  }

在每个出现mutex 的函数中,只能使用这种模式访问它。 然而......我遇到了僵局。这怎么可能?

注意:这段代码已经在生产服务器上运行了 10 个月,这是我第一次得到它。

编辑:因此 f1() 可以(间接)调用 f2() 以根据答案获得死锁。没错,但在我的代码中这不会发生,所以我真的很想知道

【问题讨论】:

  • f1 是否保证退出?它会调用其他锁定互斥锁的函数吗?
  • 如果cache 的一个方法调用另一个方法,并且都包含Lock() 调用,则很容易发生死锁。
  • 如上,取决于f1() 的作用。此外,您无需创建新的互斥体并使用Init()。只需在结构中使用sync.Mutex 而不是*sync,Mutex,因为设计的零值是有效的未锁定互斥体。
  • 你在比赛检测器下运行过这个吗?我不确定它是否可以检测到可能的多次锁定尝试,但它可能会揭示一些东西......
  • @Thomas 方法不需要直接相互调用。可能是cache.f1() 调用foo() 这是一个“独立”函数,如果foo() 调用cache.f2(),我们处于同样的死锁状态。请参阅编辑后的答案。如果你的代码中甚至不存在这样的传递调用,那么你需要发布一个minimal reproducible example,否则它就会偏离主题。

标签: go deadlock


【解决方案1】:

如果cache 的一个方法调用另一个方法,并且都包含Lock() 调用,则很容易发生死锁。

看这个例子:

func (this *cache) f1() {
    this.mutex.Lock()
    defer this.mutex.Unlock()
    this.f2()
}

func (this *cache) f2() {
    this.mutex.Lock()
    defer this.mutex.Unlock()
}

func main() {
    c := &cache{}
    c.Init()
    c.f1()
    fmt.Println("Hello, playground")
}

输出(在Go Playground 上试试):

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0x1040a12c, 0x8)
    /usr/local/go/src/runtime/sema.go:62 +0x40
sync.(*Mutex).Lock(0x1040a128, 0x10429f5c)
    /usr/local/go/src/sync/mutex.go:87 +0xa0
main.(*cache).f2(0x10429f94, 0x1100c0)
    /tmp/sandbox647646735/main.go:23 +0x40
main.(*cache).f1(0x10429f94, 0xdf6e0)
    /tmp/sandbox647646735/main.go:19 +0xa0
main.main()
    /tmp/sandbox647646735/main.go:30 +0x60

请注意,从一种方法到另一种方法不需要直接调用,也可以是传递调用。例如cache.f1() 可能会调用foo(),这可能是一个“独立”函数,如果foo() 调用cache.f2(),我们就会陷入同样的​​死锁。

改进:

不要将您的接收者命名为this,这不是惯用语。你可以简单地称它为c。在此处阅读更多信息:In Go is naming the receiver variable 'self' misleading or good practice?

您可以嵌入互斥锁,使其使用方便,无需初始化。在此处阅读更多信息:When do you embed mutex in struct in Go?

type cache struct {
    sync.Mutex
}

func (c *cache) f1() {
    c.Lock()
    defer c.Unlock()
    c.f2()
}

func (c *cache) f2() {
    c.Lock()
    defer c.Unlock()
}

func main() {
    c := &cache{}
    c.f1()
    fmt.Println("Hello, playground")
}

当然这也会导致死锁。在Go Playground 上试试。另请注意,这固有地公开了互斥锁(因为嵌入类型以 lowecae 字母开头),因此任何人都可以调用 Lock()Unlock() 方法。这取决于情况是否有问题。

【讨论】:

  • 它只在包级别公开互斥锁,因为它没有被导出。正确的?只是为 OP 澄清一下,否则可能会造成混淆。
  • @reticentroot 不,这不是真的。在包内,所有包级标识符都是可见的。在它之外,仅导出标识符。 cache 类型本身没有被导出,但是例如如果导出的函数返回一个类型为cache*cache 的值,那么任何人都可以调用它的Lock() 方法。
  • 啊,是的,您有权嵌入互斥锁,您可以通过使用互斥锁类型的未导出成员来规避这一点。当然初始化必须再次处理......所以我想这就是好的文档进来的地方哈哈。无论如何都可以抽象来“隐藏”锁吗?
  • @reticentroot 在某些情况下可以,例如您使用非指针字段,但请确保使用指向包装结构的指针以避免意外复制互斥体,并确保该字段是可寻址的,因为需要该地址才能调用 Lock()Unlock() 方法有指针接收器。这可以在没有初始化的情况下实现,因为 sync.Mutex 的零值是一个有效的、未锁定的互斥体。
猜你喜欢
  • 2012-01-16
  • 1970-01-01
  • 1970-01-01
  • 2019-12-27
  • 1970-01-01
  • 2018-04-20
  • 2012-09-20
  • 2018-06-04
  • 2016-12-22
相关资源
最近更新 更多