一.简介
如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信系统,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特定的类型,也就是channels可发送数据的类型。
二.使用
2.1 创建channel
ch := make(chan int)
2.2 channel读写
ch := make(chan int)
// write to channel
ch <- x
// read from channel
x <- ch
// another way to read
x = <- ch
channel 一定要初始化后才能进行读写操作,否则会永久阻塞。
2.3 关闭 channel
ch := make(chan int)
close(ch)
有关 channel 的关闭,你需要注意以下事项:
- 关闭一个未初始化(nil) 的 channel 会产生 panic
- 重复关闭同一个 channel 会产生 panic
- 向一个已关闭的 channel 中发送消息会产生 panic
- 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
三.channel的两种类型
3.1 有缓冲channel
ch := make(chan int, 10)
有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。
3.2 无缓冲channel
ch := make(chan int)
从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。
3.3 select 使用
select 用法类似 IO 多路复用,可以同时监听多个 channel 的消息状态,看下面的例子
select {
case <- ch1:
...
case <- ch2:
...
case ch3 <- 10;
...
default:
...
}
msgCh := make(chan struct{})
quitCh := make(chan struct{})
for {
select {
case <- msgCh:
doWork()
case <- quitCh:
finish()
return
}
- select 可以同时监听多个 channel 的写入或读取
- 执行 select 时,若只有一个 case 通过(不阻塞),则执行这个 case 块
- 若有多个 case 通过,则随机挑选一个 case 执行
- 若所有 case 均阻塞,且定义了 default 模块,则执行 default 模块。若未定义 default 模块,则 select 语句阻塞,直到有 case 被唤醒。
- 使用 break 会跳出 select 块。
三.底层实现
2.1 hchan结构
type hchan struct {
qcount uint // 队列中当前数据的个数
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // 数据缓冲区,存放数据的环形数组
elemsize uint16 // channel中数据类型的大小(单个元素的大小)
closed uint32 // 表示channel是否关闭标识位
elemtype *_type // 队列中的元素类型
sendx uint // 当前发送元素的索引
recvx uint // 当前接收元素的索引
recvq waitq // 接受等待队列,由recv行为(也就是<-ch)阻塞在channel上的goroutine队列
sendq waitq // 发送等待队列, 由send行为(也就是ch<-)阻塞在channel上的goroutine队列
//lock保护chann中的所有字段,以及在此通道上阻塞的sudoG中的几个字段。
//保持此锁时不要更改另一个G状态(特别是没准备好G),因为这可能会因堆栈收缩而死锁
lock mutex
}
//发送及接收队列的·1结构体
type waitq struct {
first *sudog
last *sudog
}
- qcount uint // 当前队列中剩余元素个数。
- dataqsiz uint // 环形队列长度,即缓冲区的大小,即make(chan T,N),N。
- buf unsafe.Pointer // 环形队列指针。
- elemsize uint16 // 每个元素的大小。
- closed uint32 // 表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。
- elemtype *_type // 元素类型,用于数据传递过程中的赋值。
- sendx uint 和 recvx uint是环形缓冲区的状态字段,它指示缓冲区的当前索引 - 支持数组,它可以从中发送数据和接收数据。
- recvq waitq // 等待读消息的goroutine队列。
- sendq waitq // 等待写消息的goroutine队列。
- lock mutex // 互斥锁,为每个读写操作锁定通道,因为发送和接收必须是互斥操作。
2.2 创建过程
2.2.1 写入操作
1.创建带buffer的channel
2.向channel中写入数据
3.3 写入过程如下:
- 锁定整个管道结构。
- 确定写入,尝试从等会带队列等待goroutine,然后将元素直接写入goroutine。
- 如果recvq为空,则确定缓冲区是否可用。如果可用,从当前goroutine复制数据到缓冲区。
- 如果缓冲区已满,则要写入的元素将保存在当前正在执行的goroutine结构中,并且当前goroutine将在sendq中排队并从运行中挂起。
- 写入完成释放锁。
2.2.2 读取过程
- 先读取channel全局锁。
- 尝试sendq从等待队列中获取等待的goroutine。
- 如果有等待的goroutine,且有缓冲区(缓冲区已满),从缓冲区队首取出数据,再从sendq取出一个goroutine。将goroutine中数据存入buf对位,结束读取释放锁。
- 如没有后等待的goroutine,且缓冲区有数据,直接读取缓冲区数据,解释读取释放锁。
- 如果没有等待的goroutine,且没有缓冲或缓冲区域为空,将当前的goroutine加入denq排队,进入睡眠,等待被写goroutine唤醒。结束释放锁。