这个模式如何导致死锁?

huangapple go评论83阅读模式
英文:

How can this pattern result in a deadlock?

问题

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

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

func (this *cache) Init() {  // 确保在main()函数中只调用一次
    this.mutex = &sync.Mutex{}
}

func (this *cache) f1() {
    // 在每个需要的'cache'函数的顶部使用的访问mutex的模式
    this.mutex.Lock()
    defer this.mutex.Unlock()
    ...
}

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

在每个函数中,mutex都是按照这种模式访问的。然而...我遇到了死锁。这怎么可能?

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

编辑:根据回答,f1()可以间接调用f2()导致死锁。但在我的代码中,这种情况并没有发生,所以我真的很好奇。

英文:

I have a (LRU) cache object and I encounter a deadlock... How is this possible?

  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()
     ...
  }

In every function mutex appears, it is accessed with this pattern only.
And yet... I get a deadlock. How come this is possible?

Note: This code has been running on a production server for 10 months and it is the first time I get that.

EDIT: so f1() can call (indirectly) f2() to get a deadlock based on the answers. True but in my code this doesn't happen, so I really wonder

答案1

得分: 5

死锁可能很容易发生,如果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。在这里阅读更多相关信息:https://stackoverflow.com/questions/23482068/in-go-is-naming-the-receiver-variable-self-misleading-or-good-practice/23494386#23494386

你可以嵌入互斥锁,使其使用更方便,并消除初始化的需要。在这里阅读更多相关信息:https://stackoverflow.com/questions/44949467/when-do-you-embed-mutex-in-struct-in-go/44950096#44950096

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上尝试一下。还要注意,这会固有地暴露互斥锁(因为嵌入类型以小写字母开头),因此任何人都可以调用Lock()Unlock()方法。具体情况取决于是否是问题。

英文:

Deadlock may easily occur if one method of cache calls another method, and both contain the Lock() call.

See this example:

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")
}

Output (try it on the 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

Note that there does not need to have a direct call from one method to the other, it may also be a transitive call. For example cache.f1() may call foo() which may be a "standalone" function, and if foo() calls cache.f2(), we're at the same deadlock.

Improvements:

Don't name your receiver this, it is not idiomatic. You may simply call it c. Read more about it here: https://stackoverflow.com/questions/23482068/in-go-is-naming-the-receiver-variable-self-misleading-or-good-practice/23494386#23494386

You may embed mutexes, making it convenient to use and eliminate the need for initialization. Read more about it here: https://stackoverflow.com/questions/44949467/when-do-you-embed-mutex-in-struct-in-go/44950096#44950096

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")
}

Of course this also causes a deadlock. Try it on the Go Playground. Also note that this inherently exposes the mutex (as the embedded type starts with lowecae letter), so anyone will be able to call the Lock() and Unlock() methods. Depends on case whether this is a problem.

huangapple
  • 本文由 发表于 2017年7月25日 22:37:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/45306497.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定