一、数据结构和底层
Go 语言的 Channel 在运行时使用 runtime.hchan 结构体表示。我们在 Go 语言中创建新的 Channel 时,实际上创建的都是如下所示的结构:
type hchan struct {
qcount uint // 队列中元素总数量
dataqsiz uint // 循环队列的长度
buf unsafe.Pointer // 指向长度为 dataqsiz 的底层数组,只有在有缓冲时这个才有意义
elemsize uint16 // 能够发送和接受的元素大小
closed uint32 // 是否关闭
elemtype *_type // 元素的类型
sendx uint // 当前已发送的元素在队列当中的索引位置
recvx uint // 当前已接收的元素在队列当中的索引位置
recvq waitq // 接收 Goroutine 链表
sendq waitq // 发送 Goroutine 链表
lock mutex // 互斥锁
}
// waitq 是一个双向链表,里面保存了 goroutine
type waitq struct {
first *sudog
last *sudog
}
如下图所示,channel 底层其实是一个循环队列:
二、发送数据
当想要向 Channel
发送数据时,就需要使用 ch <- i
语句.
在发送数据的逻辑执行之前会先为当前 Channel 加锁,防止多个线程并发修改数据。
如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 “send on closed channel” 错误并中止程序。
2.1 直接发送
如果 Channel 没有被关闭并且已经有处于读等待的 Goroutine,会取出最先陷入等待的 Goroutine 并直接向它发送数据:
直接发送的过程称为两个部分:
- 调用
runtime.sendDirect
将发送的数据直接拷贝到 x = <-c 表达式中变量 x 所在的内存地址上; - 调用
runtime.goready
将等待接收数据的 Goroutine 标记成可运行状态 Grunnable 并把该 Goroutine 放到发送方所在的处理器的 runnext 上等待执行,该处理器在下一次调度时会立刻唤醒数据的接收方;
需要注意的是,发送数据的过程只是将接收方的 Goroutine 放到了处理器的 runnext 中,程序没有立刻执行该 Goroutine。
2.2 缓冲区
如果创建的 Channel 包含缓冲区并且 Channel 中的数据没有装满,会使用 runtime.chanbuf
计算出下一个可以存储数据的位置,然后通过 runtime.typedmemmove
将发送的数据拷贝到缓冲区中并增加 sendx 索引和 qcount 计数器。
2.3 阻塞发送
当 Channel 没有接收者能够处理数据时,向 Channel 发送数据会被下游阻塞,当然使用 select 关键字可以向 Channel 非阻塞地发送消息。
2.4 小结
可以简单梳理和总结一下使用 ch <- i
表达式向 Channel 发送数据时遇到的几种情况:
- 如果当前 Channel 的 recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前 Goroutine 并将其设置成下一个运行的 Goroutine;
- 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们会直接将数据存储到缓冲区 sendx 所在的位置上;
- 如果不满足上面的两种情况,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据;
三、接收数据
可以使用两种不同的方式去接收 Channel 中的数据:
i <- ch
i, ok <- ch
3.1 直接接收
会根据缓冲区的大小分别处理不同的情况
- 如果 Channel 不存在缓冲区,直接从发送者那里把数据拷贝给接收变量
- 如果是有缓冲 channel
- 将队列中的数据拷贝到接收方的内存地址;
- 将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;
3.2 缓冲区
当 Channel 的缓冲区中已经包含数据时,从 Channel 中接收数据会直接从缓冲区中 的索引位置中取出数据进行处理:
3.3 阻塞接收
当 Channel 的发送队列中不存在等待的 Goroutine 并且缓冲区中也不存在任何数据时,从管道中接收数据的操作会变成阻塞的,然而不是所有的接收操作都是阻塞的,与 select 语句结合使用时就可能会使用到非阻塞的接收操作。
四、关闭channel
使用 close(ch) 来关闭 channel 最后会调用 runtime 中的 closechan 方法.
- 关闭一个 nil 的 channel 和已关闭了的 channel 都会导致 panic
- 关闭 channel 后会释放所有因为 channel 而阻塞的 Goroutine
五、应用
根据控制Channel的缓存大小来控制并发执行的Goroutine的最大数目
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func() {
limit <- 1
w()
<-limit
}()
}
select{}
}
最后一句select{}是一个空的管道选择语句,该语句会导致main线程阻塞,从而避免程序过早退出。还有for{}
、<-make(chan int)
等诸多方法可以达到类似的效果。因为main线程被阻塞了,如果需要程序正常退出的话可以通过调用os.Exit(0)实现。
参考链接:十.Go并发编程--channel使用