01
介绍
Go 语言使用 goroutine
和 channel
,可以实现通过通信共享内存。
本文我们介绍 Go 语言怎么通过通信共享内存。
02
goroutine
和 channel
在了解 Go 语言怎么通过通信共享内存之前。我们需要先了解一些预备知识,即 goroutine
和 channel
是什么?
goroutine
:
goroutine
具有简单的模型:它是与其它 goroutine
并发运行在同一地址空间的函数。
goroutine
是轻量级的,所有消耗几乎就只有栈空间的分配。而且栈最开始是非常小的,所以他们很廉价,仅在需要时才会随着堆空间的分配(和释放)而变化。
摘自「Effective Go - channels[1]」。
注意:goroutine
之所以取名为goroutine
,是因为现有的术语 - 线程、协程、进程等等 - 无法准确传达它的含义。也有些资料将goroutine
翻译为 Go 协程或 Go 程。
使用 goroutine
也非常简单,在函数或方法前添加 go
关键字,即可在新的 goroutine
中调用它。当调用完成后,该 goroutine
也会安静地退出。
此外,匿名函数也可以在 goroutine
中调用。
关于 goroutine
的实现原理和调度器模型 GPM,感兴趣的读者朋友们可以自行查阅相关资料。
channel
:
我们已了解,什么是 goroutine
,以及怎么使用 goroutine
调用函数或方法、匿名函数。
但是,想要实现 goroutine
之间的通信,我们还需要了解 channel
。
channel
需要使用内置函数 make
分配内存,其结果值充当了对底层数据结构的引用。如果提供了一个可选的参数,它就会为该 channel
设置缓冲区大小,否则,该 channel
则为无缓冲区的 channel
。
关于 channel
的实现原理,感兴趣的读者朋友们可以阅读「Golang 语言中的 channel 实现原理」。
需要注意的是,两个 goroutine
之间通过无缓冲区的 channel
通信时,同步交换数据。
作为两个 goroutine
之间的通信管道,向 channel
中发送数据的 goroutine
称为“发送者”,反之,从 channel
中接收数据的 goroutine
称为“接收者”。
03
通过通信共享内存
我们已经基本了解 Go 语言的 goroutine
和 channel
,接下来我们看一下两个 goroutine
之间怎么使用 channel
(无缓冲区和缓冲区)进行通信?
无缓冲区 channel
:
示例代码:
func main() {
c := make(chan int) // 定义一个无缓冲区 channel
go func() { // 启动一个 goroutine 调用匿名函数
fmt.Println("启动一个 goroutine 调用匿名函数")
c <- 1 // 该 goroutine 向 channel 发送一个值(信号)
}()
fmt.Println("main 函数")
out := <-c // main goroutine 从 channel 中接收一个值(信号),再未接收到值(信号)之前,一直阻塞
fmt.Println(out) // 该打印无实际意义,仅为了读者容易理解
}
阅读上面这段代码,我们定义一个无缓冲区 channel
,执行匿名函数的 goroutine
作为发送者,main goroutine
作为接收者。
需要注意的是,无缓冲区 channel
,接收者在收到值之前,发送者会一直阻塞。同理,发送者在发送值之前,接收者也会一直阻塞。
缓冲区 channel
:
示例代码:
func main() {
// c := make(chan int) // 无缓冲区 channel
c := make(chan int, 5) // 缓冲区 channel
for i := 0; i < 20; i++ {
c <- 1
go func() {
fmt.Println("do something:", i)
<-c
}()
}
time.Sleep(time.Second * 2) // 为了防止 main goroutine 提前退出
}
阅读上面这段代码,我们定义一个缓冲区大小为 5 的 channel
,执行匿名函数的 goroutine
作为接收者,main goroutine
作为发送者。
需要注意的是,该段代码中有 5 个执行匿名函数的 goroutine
,即 N 个接收者,1 个发送者(main goroutine
)。
我们前面讲过,接收者在收到值之前会一直阻塞,而无缓冲区 channel
在接收者收到值之前,发送者会一直阻塞。
如果我们将上面这段代码中的缓冲区 channel
换成无缓冲区 channel
,N - 1
个接收者在接收到值之前,发送者会一直阻塞,发送者阻塞,导致接收者一直接收不到值,也会一直阻塞,从而导致死锁。
上面这段话有些拗口,读者朋友们可以通过运行使用无缓冲区 channel
的代码来帮助自己理解。
我们运行使用缓冲区大小为 5 的 channel
的代码,发现代码可以正常运行,发送者和接收者之间不会产生死锁。
这是因为缓冲区 channel
,发送者仅在值被复制到缓冲区之前阻塞,如果缓冲区已满,发送者会一直阻塞,直到某个接收者取出一个值。
回到上面这段示例代码中,执行匿名函数的 N 个 goroutine
作为接收者,在没有收到 main goroutine
发送的数据之前,一直处于阻塞状态,直到作为发送者的 main goroutine
发送数据到缓冲区 channel
中。
读者朋友们如果仔细阅读这段代码,会发现上面这段代码虽然不会产生死锁,但是存在一个 bug
。
解决方案可以阅读我们之前的一篇文章「Go 语言使用 goroutine 运行闭包的“坑”」,限于篇幅,我就不在本文中赘述了。
04
总结
本文我们介绍 Go 语言中,什么是 goroutine
和 channel
,其中 channel
分为无缓冲区和缓冲区。
在简单了解 goroutine
和 channel
后,我们又介绍怎么使用 channel
,实现两个 goroutine
之间通信。
参考资料
[1] Effective Go - channels: https://go.dev/doc/effective_go#goroutines