go语言虽然号称协程之间必须使用channel通信,但是如果使用不当,非常容易形成deadlock死锁。下面的代码就是这样的一个例子
package main
import "fmt"
func doWork(id int, dataChan chan int, done chan bool) {
for data := range dataChan {
fmt.Printf("Worker %d received %c\n", id, data)
done <- true
}
}
func createChannels(id int) Worker {
ch := Worker{
data: make(chan int),
done: make(chan bool),
}
go doWork(id, ch.data, ch.done)
return ch
}
func chanRun() {
var channels [10]Worker
for i := 0; i < 10; i++ {
channels[i] = createChannels(i)
}
for i, ch := range channels {
ch.data <- 'a' + i
//<-ch.done
}
for i, ch := range channels {
ch.data <- 'A' + i
//<-ch.done
}
for _, ch := range channels {
<-ch.done
<-ch.done
}
}
type Worker struct {
data chan int
done chan bool
}
func main() {
chanRun()
}
为什么会形成死锁? 这里需要对无缓冲channel有深刻的理解。
无缓冲channel需要发送方和接收方同时在线,只要有一方没有就位,就会引发死锁!
在在doWork这段代码里,dataChan和done这两个通道是耦合在一起了,因为他们在一个同步代码块里,所以会出现互相等待的问题。 一旦其中一个通道被阻塞,另外一个通道也会因为等待陷入无法就绪状态,从而产生deadlock死锁问题。
这里的解决死锁的关键就是解耦,也就是将两个channel的因果关系破坏掉,避免互相等待。
具体来说:可以给其中的一个channel操作再开一个协程,也就是并发执行,这样就脱离了同步代码块,在这个例子里面,如下修改即可。
done <- true ==》 go func() { done <- true }()
func doWork(id int, dataChan chan int, done chan bool) {
for data := range dataChan { // 注意range是阻塞式读取channel,所以后面的done <- true如果不开协程也会被阻塞住
fmt.Printf("Worker %d received %c\n", id, data)
//给done这个channel单独再可一个协程,并发执行,脱离doWork主程序,
// 从而避免dataChan和done这两个channel在同步代码块中的互相等待
go func() { done <- true }()
//done <- true
}
}
还有一个解决办法: 将两个通道中任意一个通道变成有缓冲通道
ch := Worker{ data: make(chan int,1), done: make(chan bool), }或者
ch := Worker{ data: make(chan int), done: make(chan bool,1), }
此时done通道也就无需单独开协程。
因此我们使用无缓冲channel时一定要非常小心,为避免死锁, 总体有以下两大原则:
1. 尽量避免在同步代码中使用两个channel的情况,避免channel之间出现耦合、嵌套的情况,因为容易出现两个channel互相等待的问题; 如果确实没法避免,则必须给其中一个channel单独再开一个协程,类似于下面的代码:
go func() { channel <- data }() // 通过匿名函数给channel再单独开一个协程
2. 往无缓冲channel发送数据时务必单独再开一个协程
如果没有把握处理好无缓冲channle,为了安全起见,建议使用有缓冲channel
顺附:有缓冲channel和无缓冲channel的区别说明
有缓冲channel和无缓冲channel本质上就是一个同步和异步的区别。
无缓冲:就是完全同步,发送和接收方紧紧耦合在一起。
有缓冲: 异步工作。 通道未满之前,发送方和接收方各自独立工作:也就是都不会阻塞。这个有点类似于消息队列MQ.此时通道起到了解耦的作用。通道满了以后,就是同步通信,此时发送方阻塞。