go channel 学习笔记
CSP
CSP 是 Communicating Sequential Processes(通信顺序进程)的简称。其理论是通过共享内存以外的通信方式来协调并发实体。在 golang 中通过 channel 和 goroutine 实现了其中很小一部分。
channel 是什么
channel 是 goroutine 之间用于沟通的管道。
channel 基础用法
channel 的初始化非常简单:
ch := make(chan channelType) // 无缓冲管道
ch := make(chan channelType, size) // 有缓冲管道
这里有一个缓冲的概念,缓冲说白了就是管道内能存多少个数据。无缓冲管道内不能存任何数据,因此在写的时候,如果没有 goroutine 在读,那么将会阻塞,相反读的时候如果没有 goroutine 在写也是如此。
而有缓冲时,如果容量未满,向管道写入就可以不被阻塞。当然如果容量满了依然会被阻塞,容量为空对于读者来说也会被阻塞。
举一个例子来展示 channel 的基本用法,请看下面这段代码:
func main(){
ch := make(chan int)
go func(){
for i := 0; i< 5;i++{
num := <-ch
fmt.Printf("sub func: get %d\n", num)
}
}()
for i:=0; i< 5;i++{
ch <- i
fmt.Println("main func: %d has been send", i)
}
}
最后的输出可能是:
sub func: get 0
main func: 0 has been send
sub func: get 1
main func: 1 has been send
sub func: get 2
main func: 2 has been send
sub func: get 3
main func: 3 has been send
sub func: get 4
main func: 4 has been send
当然,因为 println 和管道读写不是原子操作,因此输出可能会有略微变化。但是大概自已就是这样的了。
上面这个例子展示了最简单的一种读写情况,管道还支持使用 range 进行遍历读取,比如:
func main(){
ch := make(chan int)
go func(){
for num := range ch{
fmt.Printf("sub func: get %d\n", num)
}
}()
for i:=0; i< 5;i++{
ch <- i
fmt.Println("main func: %d has been send", i)
}
}
for range 的退出条件是:缓冲区无数据 + 管道被关闭。因此这里的 for range 在这段代码中实际上不会退出,直到 main goroutine 退出之后,其 goroutine 也被关闭。
管道关闭的方式也比较简单,只需要调用 close 即可:
close(ch)
但注意,如果一个 channel 已经 close 了,那么继续写入将会 panic,读取的情况没有那么糟糕,如果是一个有缓冲的 channel,其中还有数据是可以继续读出的,如果数据已空,继续读也只会读出 0 值。
| 操作 | 无缓冲 channel | 有缓冲 channel |
|---|---|---|
写ch <- x |
❌ panicsend on closed channel |
|
读<-ch |
✅ 返回零值 (不会阻塞) |
缓冲区有数据:✅ 正常读取数据 缓冲区空:✅ 返回零值 (都不会阻塞) |
但是这里有个问题,有时候存入的数据也是零值,因此为了避免和存入的零值搞混,除了刚才举的 for range 的方法以外,在读取的时候可以使用 ok 来判定是否为零值:
func main() {
ch := make(chan int)
go func() {
for {
num, ok := <-ch
if !ok {
fmt.Println("sub func: channel 已关闭,退出")
return
}
fmt.Printf("sub func: get %d\n", num)
}
}()
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("main func: %d has been send\n", i)
}
close(ch)
// 让sub func有时间打印退出信息
time.Sleep(time.Millisecond * 100)
}
这里用 ok 来判断是否已经关闭了。
单向 channel
单向 channel 是一种特殊类型,用来限制 channel 数据的操作方向。它是这样定义的:
ch :=make(chan int, 5)
var Wch chan<- int = ch
var Rch <-chan int = ch
在这样定义之后,错误的使用将产生编译错误。这让 channel 使用更清晰。
channel 原理
channel的原理要从其数据结构讲起。channel实际上是如下数据结构:
type hchan struct {
qcount uint // 队列中所有数据总数
dataqsiz uint // 环形队列的 size
buf unsafe.Pointer // 指向 dataqsiz 长度的数组
elemsize uint16 // 元素大小
closed uint32
elemtype *_type // 元素类型
sendx uint // 已发送的元素在环形队列中的位置
recvx uint // 已接收的元素在环形队列中的位置
recvq waitq // 接收者的等待队列
sendq waitq // 发送者的等待队列
lock mutex
}
其中,buf是存储数据的缓冲区,长度为dataqsiz。发送和接受的位置由sendx和recvx来维护。 当缓冲区满或者缓冲区为空时,存在管理接收者和发送者的双向队列recvq和sendq。 其他的一切就非常简单了,在其他博客中非常详细的描述了具体的细节,不过简单说就只有接收和发送两个流程。
发送数据流程
- 获取chan的锁。
- 检查是否closed,如果是则panic。
- 如果recvq中有G,那么直接将数据拷贝给该G,并唤醒它。
- 如果缓冲区中有数据且缓冲区未满,那么拷贝到sendx处,并维护相关的元素。
- 如果缓冲区已满,则把自己挂在sendq上。
接收数据流程
- 获取chan的锁
- 检查是否closed,如果是且缓冲区为空则返回零值。
- 如果snedq中有G,那么直接将它要发送的数据数据拷贝过来,并唤醒它。
- 如果缓冲区中有数据,那么拷贝recvx处的数据,并维护相关的元素。
- 如果缓冲区为空,则把自己挂在recvq上。
剩余的select相关的,等到单开一篇笔记好了。
参考
- https://developer.aliyun.com/article/1687354
- https://golangstar.cn/go_series/go_principles/channel_principles.html