死锁是所有并发进程都在彼此等待的状态。 在这种情况下,如果没有外部干预,程序将永远不会恢复。 如果这听起来很严峻,那是因为它确实很严峻! 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://box.kancloud.cn/b3afc3ec1b64f6d2e1ab8047ee501829_482x156.png) 实质上,我们创建了两个不能一起运转的齿轮:我们的第一个打印总和调用a锁定,然后尝试锁定b,但与此同时,我们打印总和的第二个调用锁定了b并尝试锁定a。 两个goroutine都无限地等待着彼此。 >为了保持这个例子简单,我使用time.Sleep来触发死锁。 但是,这引入了竞争条件! 你能找到它吗? >一个逻辑上“完美”的死锁将需要正确的同步。 这似乎很明显,为什么当我们以这种方式绘制图表时出现这种僵局,但我们会从更严格的定义中受益。事实证明,出现僵局时必定存在一些条件,1971年,埃德加科夫曼在一篇论文中列举了这些条件。这些条件现在称为科夫曼条件,是帮助检测,防止和纠正死锁的技术基础。 科夫曼条件如下: #### *相互排斥* 并发进程在任何时候都拥有资源的独占权。 #### *等待条件* 并发进程必须同时持有资源并等待额外的资源。 #### *没有抢占* 并发进程持有的资源只能由该进程释放,因此它满足了这种情况。 #### *循环等待* 并发进程(P1)等待并发进程(P2),同时P2也在等待P1,因此也符合"循环等待"这一条件。 :-: ![科夫曼条件](https://box.kancloud.cn/c290764bba3ed54f00f032741fdc47e5_386x144.png) 让我们来看看我们的设计程序,并确定它是否符合所有四个条件: 1. printSum函数确实需要a和b的独占权,所以它满足了这个条件。 2. 因为printSum保持a或b并等待另一个,所以它满足这个条件。 3. 我们没有任何办法让我们的goroutine被抢占。 4. 我们第一次调用printSum正在等待我们的第二次调用,反之亦然。 很好,我们亲手实现了死锁。 科夫曼条件同样有助于我们规避死锁。如果我们确保至少有一个条件不成立,就可以防止发生死锁。不幸的是,实际上这些条件很难推理,因此难以预防。网上大量充斥着被死锁困扰的开发人员的求助,一旦有人指出它就很明显,但通常需要另一双眼睛。 * * * * * 学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。 感谢beego群(258969317)的"赤脚大仙"提出"循环等待"部分的修改意见,文字已作调整。