1.5.6 基于Channel的通信
Channel通信是在Goroutine之间进行同步的主要方法。在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对,发送和接收操作通常发生在不同的Goroutine上(在同一个Goroutine上执行2个操作很容易导致死锁)。无缓存的Channel上的发送操作总在对应的接收操作完成前发生.
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "你好, 世界"
done <- true
}
func main() {
go aGoroutine()
<-done
println(msg)
}可保证打印出“hello, world”。该程序首先对msg进行写入,然后在done管道上发送同步信号,随后从done接收对应的同步信号,最后执行println函数。
若在关闭Channel后继续从中接收数据,接收者就会收到该Channel返回的零值。因此在这个例子中,用close(c)关闭管道代替done <- false依然能保证该程序产生相同的行为。
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "你好, 世界"
close(done)
}
func main() {
go aGoroutine()
<-done
println(msg)
}对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前。
基于上面这个规则可知,交换两个Goroutine中的接收和发送操作也是可以的(但是很危险):
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "hello, world"
<-done
}
func main() {
go aGoroutine()
done <- true
println(msg)
}也可保证打印出“hello, world”。因为main线程中done <- true发送完成前,后台线程<-done接收已经开始,这保证msg = "hello, world"被执行了,所以之后println(msg)的msg已经被赋值过了。简而言之,后台线程首先对msg进行写入,然后从done中接收信号,随后main线程向done发送对应的信号,最后执行println函数完成。但是,若该Channel为带缓冲的(例如,done = make(chan bool, 1)),main线程的done <- true接收操作将不会被后台线程的<-done接收操作阻塞,该程序将无法保证打印出“hello, world”。
对于带缓冲的Channel,对于Channel的第K个接收完成操作发生在第K+C个发送操作完成之前,其中C是Channel的缓存大小。 如果将C设置为0自然就对应无缓存的Channel,也即使第K个接收完成在第K个发送完成之前。因为无缓存的Channel只能同步发1个,也就简化为前面无缓存Channel的规则:对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前。
我们可以根据控制Channel的缓存大小来控制并发执行的Goroutine的最大数目, 例如:
var limit = make(chan int, 3)
var work = []func(){
func() { println("1"); time.Sleep(1 * time.Second) },
func() { println("2"); time.Sleep(1 * time.Second) },
func() { println("3"); time.Sleep(1 * time.Second) },
func() { println("4"); time.Sleep(1 * time.Second) },
func() { println("5"); time.Sleep(1 * time.Second) },
}
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}在循环创建Goroutine过程中,使用了匿名函数并在函数中引用了循环变量w,由于w是引用传递的而非值传递,因此无法保证Goroutine在运行时调用的w与循环创建时的w是同一个值,为了解决这个问题,我们可以利用函数传参的值复制来为每个Goroutine单独复制一份w。
循环创建结束后,在main函数中最后一句select{}是一个空的管道选择语句,该语句会导致main线程阻塞,从而避免程序过早退出。还有for{}、<-make(chan int)等诸多方法可以达到类似的效果。因为main线程被阻塞了,如果需要程序正常退出的话可以通过调用os.Exit(0)实现。
学员评价