文章目录
- 1 概念
- 2 分类
- 3 操作
- 3.1 channel 的创建
- 3.1.1 无缓冲channel
- 3.1.1 带缓冲channel
- 3.2 channel的读写
- 3.3 channel的关闭
- 3.4 channel 和 select
- 4 channel 底层原理
1 概念
channel 是一个通道,用于端到端的数据传输,这有点像我们平常使用的消息队列,只不过 channel 的发送方和接受方是 goroutine 对象,属于内存级别的通信。
2 分类
3 操作
在深入了解 channel 的底层之前,我们先来看看 channel 的常用用法。
3.1 channel 的创建
3.1.1 无缓冲channel
ch := make(chan int)
对于无缓冲的 channel,一旦有 goroutine 往 channel 写入数据,那么当前的 goroutine 会被阻塞住,直到有其他的 goroutine 消费了 channel 里的数据,才能继续运行写入数据。
3.1.1 带缓冲channel
还有另外一种是有缓冲的 channel,它的创建是这样的:
ch := make(chan int, 10)
其中,第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,则当前的 goroutine 就不会被阻塞住。
另外,我们也可以声明一个 nil 的 channel,只是创建这样的 channel 没有意义,读、写 channel 都将会被阻塞住。一般 nil channel 用在 select 上,让 select 不再从这个 channel 里读取数据,如下用法:
ch1 := make(chan int)
ch2 := make(chan int)
go func() {if !ok { // 某些原因,设置 ch1 为 nilch1 = nil}}()
for {select {case <-ch1: // 当 ch1 被设置为 nil 后,将不会到达此分支了。doSomething1()case <-ch2:doSomething2()}
}
3.2 channel的读写
3.3 channel的关闭
ch := make(chan int,10)...
close(ch)
面试题:当关闭channel之和再操作channel会发生什么?
写数据:则程序会直接 panic 退出;
读数据:(1) 有数据:读到关闭之前写入的数据;
(2) 无数据:将得到零值,即对应类型的默认值。
3.4 channel 和 select
在写程序时,有时并不单单只会和一个 goroutine 通信,当我们要进行多 goroutine 通信时,则会使用 select 写法来管理多个 channel 的通信数据:
ch1 := make(chan struct{})ch2 := make(chan struct{})// ch1, ch2 发送数据go sendCh1(ch1)go sendCh1(ch2)// channel 数据接受处理for {select {case <-ch1:doSomething1()case <-ch2:doSomething2()}}
channel 的死锁
前面提到过,往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。
然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:
func main() {ch := make(chan int)<-ch// 执行后将 panic:// fatal error: all goroutines are asleep - deadlock!}
因此,在使用 channel 时要注意 goroutine 的一发一取,避免 goroutine 永久阻塞!
4 channel 底层原理
前面提及过 channel 创建后返回了 hchan 结构体,现在我们来研究下这个结构体,它的主要字段如下:
type hchan struct {//channel分为无缓冲和有缓冲两种。//对于有缓冲的channel存储数据,借助的是如下循环队列的结构qcount uint // 循环队列中的元素数量dataqsiz uint // 循环队列的长度buf unsafe.Pointer // 指向底层循环队列的指针elemsize uint16 //能够收发元素的大小closed uint32 //channel是否关闭的标志elemtype *_type //channel中的元素类型//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置sendx uint // 下一次发送数据的下标位置recvx uint // 下一次读取数据的下标位置//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列recvq waitq // 读等待队列sendq waitq // 写等待队列lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
channel 在进行读写数据时,会根据无缓冲、有缓冲设置进行对应的阻塞唤起动作,它们之间还是有区别的。下面我们来捋一下这些不同之处。
无缓冲 channel
由于对 channel 的读写先后顺序不同,处理也会有所不同,所以,还得再进一步区分:
channel 先写再读
在这里,我们暂时认为有 2 个 goroutine 在使用 channel 通信,按先写再读的顺序,则具体流程如下:
可以看到,由于 channel 是无缓冲的,所以 G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来。
接着,又有 goroutine 来 channel 读取数据了:
此时 G2 发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。
channel 先读再写
先读再写的流程跟上面一样。
G1 暂时被挂在了 recvq 队列,然后休眠起来。
G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。
有缓冲 channel
在分析完了无缓冲 channel 的读写后,我们继续看看有缓冲 channel 的读写。同样的,我们分为 2 种情况:
channel 先写再读
这一次会优先判断缓冲数据区域是否已满,如果未满,则将数据保存在缓冲数据区域,即环形队列里。如果已满,则和之前的流程是一样的。
当 G2 要读取数据时,会优先从缓冲数据区域去读取,并且在读取完后,会检查 sendq 队列,如果 goroutine 有等待队列,则会将它上面的 data 补充到缓冲数据区域,并且也对其设置 goready 函数。
channel 先读再写
此种情况和无缓冲的先读再写是一样流程,此处不再重复说明。
四、总结
有缓冲 channel 和无缓冲 channel 的读写基本相差不大,只是多了缓冲数据区域的判断而已。
channel 在使用的时候大多时候得和 select 配合使用,尽管只需要简单的用 <- ch 和 ch <- 来读写数据,但它的底层还是很有讲究的,特别是涉及到调度的休眠唤起。
这也能看出 Go 的精妙之处:复杂底层,优雅运用。