Go语言的channel和死锁
阻塞(同步)channel–不带缓冲区的channel
阻塞channel要点
默认情况下创建的channel是阻塞和不带缓冲区的,例如:
ch1 := make(chan int) // 创建一个阻塞的不带缓冲区的channel
通过默认方式创建的channel有以下性质:
- 发送操作将会阻塞,直到接收端准备好了。
- 接收操作将会阻塞,直到发送端准备好了。也就是说:若channel中没有数据,接收者将会阻塞。
阻塞channel产生的死锁
- 死锁的例子
先看下面的程序:
package main
import (
"fmt"
)
func f1(in chan int) {
fmt.Println(<-in)
}
func main() {
out := make(chan int)
out <- 2 // 阻塞,后面的代码不会执行
go f1(out) // 这里不会执行
}
执行结果:
fatal error: all goroutines are asleep - deadlock!
死锁原因分析
为什么会出现死锁呢?分析产生这种错误的原因,有利于我们对死锁的理解。
out := make(chan int)
out <- 2 //执行这一句时,接收端还没有准备好,此时main线程阻塞了
go f1(out) // 这里不会执行
注意:执行以上两句话时,main协程阻塞,此时不会再往下执行了。由于没有其他goroutine在接收,直接报错。 所以以上程序的 go f1(out) 这一句是不会执行的,即使注释掉也会报同样的错误。
死锁的解决
如何解决该问题呢?就是要保证channel的接收端和发送端的goroutine都能得到执行。针对以上的例子,可以如下修改。
- 方法1
先启动goroutine,让主goroutine后面的代码得以执行,修改的方式如下:
func main() {
out := make(chan int)
go f1(out) // 先启动goroutine,启动后会阻塞在fmt.Println(<-in)中,但主线程继续执行
out <- 2 // 由于已经启动了goroutine,所以,这句得到了执行
}
- 方法2
接收端和发送端都通过goroutine启动,但这种方式要注意和主线程的同步。
func main() {
out := make(chan int)
go func(){out <- 2}() // 启动goroutine,并阻塞
go f1(out) // 启动goroutine,接收channel的数据
time.Sleep(1e9) // 主线程等待1秒,等待所有goroutine都完成自己的工作,但这里需要优化
}
若channel的接收和发送都使用goroutine,则代码的顺序无关紧要。
异步channel–带缓冲区的channel
异步channel基础要点
- 不带缓冲区的channel只能包含一个元素(一条记录),带缓冲区的channel可以包含多条记录
n := 100
ch1 := make(chan string, n) // 此时的ch1,类似一个消息队列,可以容纳100个string类型的元素
- 向带缓冲区的channel写数据时不会阻塞,直到channel的缓冲区满了
- 从带缓冲区的channel中读数据也不会阻塞,直到缓冲区为空
- 从带缓冲区的channel中读取或写入数据时,是异步的,类比使用消息队列写入和读取数据
- 向带缓冲区的channel中写数据时是FIFO顺序进行的
带缓冲区的channel产生的死锁
产生死锁的代码
func f2(ch chan int) {
fmt.Println(<-ch)
}
func main() {
out := make(chan int, 1) // 创建一个带缓冲区的channel,但只容纳一个元素
out <- 2 // 向channel中输入一个整数,此时channel满了没有人消费
out <- 3 // channel已满,此时阻塞
go f2(out) // 上一句已阻塞,此句不会执行
}
运行以上程序,可能得到以下结果:
fatal error: all goroutines are asleep - deadlock!
死锁的原因分析
以上代码创建了一个缓冲区的channel,该channel只能容纳一个元素。当执行out<-2时,此时的channel已经满了。当执行下一句out<-3时,由于此时的channel已经满了,而且没有其他协程消费该channel中的元素,此时阻塞,再也没有机会执行下面的语句,从而产生了死锁。
死锁的解决
- 把channel的缓冲区的容量扩大,让out<-3这一句不会阻塞,代码得以继续往下执行
out := make(chan int, 10)
- 把向channel中写入和消费数据都创建成协程,通过协程进行
func main() {
out := make(chan int, 10)
go func(){
out <- 2
out <- 3
}() // 启动goroutine,并阻塞
go f1(out) // 启动goroutine,接收channel的数据
time.Sleep(2e9) // 主线程等待1秒,等待所有goroutine都完成自己的工作
}
总结
在使用channel时,当向channel写数据时要保证有goroutine能够读出该数据,否则将会发生死锁。同样的道理,当向channel读数据时,要保证有goroutine向channel中写入数据。