死锁
最后更新于:2022-04-02 06:50:06
死锁是所有并发进程都在彼此等待的状态。 在这种情况下,如果没有外部干预,程序将永远不会恢复。
如果这听起来很严峻,那是因为它确实很严峻! Go运行时会检测到一些死锁(所有的例程必须被阻塞或“休眠”),但这对于帮助你防止死锁产生没有多大帮助。
为了帮助你更直观的认识死锁,我们先来看一个例子。同样的,跟着注释走,任何变量、函数、语句都不重要:
```
type value struct {
mu sync.Mutex
value int
}
var wg sync.WaitGroup
printSum := func(v1, v2 *value) {
defer wg.Done()
v1.mu.Lock() //1
defer v1.mu.Unlock() //2
time.Sleep(2 * time.Second) //3
v2.mu.Lock()
defer v2.mu.Unlock()
fmt.Printf("sum=%v\n", v1.value+v2.value)
}
var a, b value
wg.Add(2)
go printSum(&a, &b)
go printSum(&b, &a)
wg.Wait()
```
1. 这里我们试图访问带锁的部分
2. 这里我们试图调用defer关键字释放锁
3. 这里我们添加休眠时间 以造成死锁
如果你试着运行这段程序,应该会看到这样的输出:
```
fatal error: all goroutines are asleep - deadlock!
```
为什么? 如果仔细观察,你将在此代码中看到计时问题。下面的时序图能清晰的展现问题所在:
:-: ![死锁时序图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/b3afc3ec1b64f6d2e1ab8047ee501829_482x156.png)
实质上,我们创建了两个不能一起运转的齿轮:我们的第一个打印总和调用a锁定,然后尝试锁定b,但与此同时,我们打印总和的第二个调用锁定了b并尝试锁定a。 两个goroutine都无限地等待着彼此。
>为了保持这个例子简单,我使用time.Sleep来触发死锁。 但是,这引入了竞争条件! 你能找到它吗?
>一个逻辑上“完美”的死锁将需要正确的同步。
这似乎很明显,为什么当我们以这种方式绘制图表时出现这种僵局,但我们会从更严格的定义中受益。事实证明,出现僵局时必定存在一些条件,1971年,埃德加科夫曼在一篇论文中列举了这些条件。这些条件现在称为科夫曼条件,是帮助检测,防止和纠正死锁的技术基础。
科夫曼条件如下:
#### *相互排斥*
并发进程在任何时候都拥有资源的独占权。
#### *等待条件*
并发进程必须同时持有资源并等待额外的资源。
#### *没有抢占*
并发进程持有的资源只能由该进程释放,因此它满足了这种情况。
#### *循环等待*
并发进程(P1)等待并发进程(P2),同时P2也在等待P1,因此也符合"循环等待"这一条件。
:-: ![科夫曼条件](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/c290764bba3ed54f00f032741fdc47e5_386x144.png)
让我们来看看我们的设计程序,并确定它是否符合所有四个条件:
1. printSum函数确实需要a和b的独占权,所以它满足了这个条件。
2. 因为printSum保持a或b并等待另一个,所以它满足这个条件。
3. 我们没有任何办法让我们的goroutine被抢占。
4. 我们第一次调用printSum正在等待我们的第二次调用,反之亦然。
很好,我们亲手实现了死锁。
科夫曼条件同样有助于我们规避死锁。如果我们确保至少有一个条件不成立,就可以防止发生死锁。不幸的是,实际上这些条件很难推理,因此难以预防。网上大量充斥着被死锁困扰的开发人员的求助,一旦有人指出它就很明显,但通常需要另一双眼睛。
* * * * *
学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。
感谢beego群(258969317)的"赤脚大仙"提出"循环等待"部分的修改意见,文字已作调整。
';