问题
在 Go 语言中,线程通信方式有两种。一种是共享内存模式,另一种是消息通信模式。消息通信有四要素:发送者、接受者、通道、消息。发送者和接受者通过通道传递消息,从而实现通信。通信是线程间协作的核心。如果通信出错,则会造成发送者和接受者同时阻塞,产生死锁。
试看一道题目:
// 发送者
func main() {
ch := make(chan int)
ch <- 1 // 发送
fmt.Println("发送", 1)
// 接收者
go func() {
v := <-ch // 接收
fmt.Println("接收", v)
}()
}
复制代码
题目输出什么?
A. 发送1;接收1
B. deadlock
复制代码
解决方式
如果你执行代码,会发现程序出错,输出deadlock
。为什么会死锁?因为使用了无缓冲通道。在 Go 中,通道有两种类型,无缓存通道和有缓存通道。使用无缓存通道,稍不留神就会死锁。
无缓存通道是指无缓存空间,有缓存通道有缓存空间。除了有无缓存空间外,其阻塞策略不同。
在无缓存通道下,发送者的发送操作将阻塞,直到接收者执行接受操作。同样接受者的接受操作将阻塞,直到发送者执行发送操作。发送者的发送操作和接受者的接受操作是同步的。
在有缓存通道下,如果缓存空间满了,那么发送者的发送操作将阻塞。直到接受者取走数据,有了缓存空间,发送者的发送操作才会继续执行。如果缓存是空的,那么接受者的接受操作将阻塞。
在开始那道题中,因为使用了无缓存通道,发送者的发送操作阻塞了。发送操作后面的语句,不会执行。要想不阻塞,有两种解法。
第一种,使用无缓存通道,先创建接受者,再发送数据。 代码如下:
func main() {
ch := make(chan int)
// 接收者
go func() {
v := <-ch // 这里阻塞
fmt.Println("收到:", v)
}()
// 发送者
ch <- 1
fmt.Println("发送:", 1)
}
复制代码
第二种,使用有缓存通道。 至于创建接受者,执行发送操作的顺序,变得无关紧要了。代码如下:
func main() {
ch := make(chan int, 1)
// 发送者
ch <- 1
fmt.Println("发送:", 1)
// 接收者
go func() {
v := <-ch // 这里阻塞
fmt.Println("收到:", v)
}()
}
复制代码
原理
以上问题,在操作系统进程通信模型中,早已描述。在操作系统进程通信中,虽然发送者和接受者是进程,而不是线程,不是goroutine
。但是从通信角度来看,并无区别。不管是进程间通信,还是线程间通信,其核心是通信。在操作系统中,进程间消息通信,可以通过send()
和receive()
来进行。消息通信,可以是阻塞或非阻塞,也称为同步或异步。
很多开发者认为,阻塞、非阻塞与同步、异步意思不同,并广泛讨论。然而,与认识相反,阻塞、非阻塞与同步、异步意思相同。
所以,讨论阻塞、非阻塞与同步、异步的差别,没有意义。因为他们本就是对称概念(也就是不同词语表达相同概念)。讨论什么有意义呢?讨论是发送者阻塞,还是接受者阻塞,更有意义。阻塞、非阻塞与发送者、接受者,组合成二乘二矩阵:
在通信过程中,总共有四种状态:
- 发送者阻塞:发送者阻塞,直到消息被接受者(或者邮箱)接收。
- 发送者非阻塞:发送者发送消息,并继续操作。不会关联接收者的状态。
- 接收者阻塞:接收者阻塞,直到有消息可用。
- 接收者非阻塞:接收者收到一个有效消息或空消息。
发送者、接受者的阻塞状态,是编程中需要经常思考的事情。
原则
如何正确使用 Go 的通道进行通信?根据通道类型判断。如果使用无缓存通道,先创建发送者或接受者,再执行发送或接收操作。如果使用有缓存通道,则注意缓存空间满了,会阻塞发送操作。
除了根据通道类型判断,最重要的是熟练掌握发送者、接受者、通道的关系,以及发送操作、接收操作何时阻塞。在 Go 中,你要关注哪个goroutine是发送者,哪个goroutine是接受者?你还要关注发送者和接受者之间的通道是哪个?最后,你要关注发送操作和接收操作何时阻塞?
这些加起来,就叫做消息通信模式。