数据结构
src/runtime/chan.go:hchan
中定义了管道的数据结构:
type hchan struct {
qcount uint // chan 里元素数量
dataqsiz uint // 底层循环数组的数量
buf unsafe.Pointer // 指向底层循环数组的指针,只针对有缓冲的 channel
elemsize uint16 // chan 中元素大小
closed uint32 // chan 是否被关闭的标志
elemtype *_type // chan 中元素类型
sendx uint // 已发送元素在循环数组中的索引
recvx uint // 已接收元素在循环数组中的索引
recvq waitq // 等待接收的 goroutine 队列
sendq waitq // 等待发送的 goroutine 队列
lock mutex // 保护 hchan 中所有字段
}
从上面的数据结构可以看出来,hchan
由 缓冲队列,类型信息,协程等待队列 和 互斥锁 组成。
- 环形队列
chan 内部实现了一个环形队列作为其缓冲区,队列的长度是在创建chan
时指定的。
image.png
-
等待队列
- 从 channel 读取数据时,如果 channel 为空或者没有缓冲区,则当前协程会被阻塞,并被加入
recvq
队列。 - 从 channel 写入数据时,如果 channel 已满或没有缓冲区,则当前 协程 会被阻塞,并被加入到
sendq
队列
处于等待队列中的协程会在其他协程操作channel 时被唤醒:
- 因读阻塞的协程会被向channel 写入数据的协程唤醒
- 因写阻塞的协程会被从channel 读取数据的协程唤醒
- 从 channel 读取数据时,如果 channel 为空或者没有缓冲区,则当前协程会被阻塞,并被加入
-
类型信息
一个 channel 只能传递一种类型的值,类型信息存储在hchan
数据结构中。-
elemtype
代表类型,用于在数据传递过程中赋值 -
elemsize
代表类型大小,用于在buf中定位元素的位置
-
-
互斥锁
一个管道同时仅允许被一个协程读写,所以这里还会包含一个互斥锁。
channel 的 创建
创建 channel 的过程实际上是初始化 hchan
结构体的过程,其中类型信息和缓冲区的产固定由内置函数make
指定,buf 的大小则由元素大小和缓冲区长度共同决定。
向 channel 写入数据
关闭 channel 会把 recvq 中的协程全部唤醒,这些协程获取的数据都为对应类型的零值,同时会把 sendq 队列中的协程全部唤醒,但这些协程会触发 panic
向一个 channel 写入数据的过程如下:
- 如果缓冲区有空余位置,则将数据写入缓冲区,结束发送过程
- 如果缓冲区没有空余位置,则将当前协程加入
sendq
队列,进入睡眠并等待被读协程唤醒。
这里实现上还有个小技巧,当接收队列recvq
不为空时,说明缓冲区没有数据但有协程在等待数据,此时会把数据直接传递给recvq
的队列中的第一个协程,而不必再写入缓冲区。
向 channel 读取数据
如果一个 channel 读取数据简单过程如下:
- 如果缓冲区有数据,则从缓冲区取出数据,结束读取过程。
- 如果缓冲区中没有数据,则将当前协程中加入
recvq
队列,进入睡眠并等待被写协程唤醒。
类似的,如果等待发送队列sendq
不为空,且没有缓冲区,那么此时将直接从 sendq
队列第一个协程中获取数据。
关闭 channel
关闭 channel 时会把 recvq
中的协程全部唤醒,这些协程获取的数据都为对应类型的零值,同时会把 sendq
队列中的协程全部唤醒,但这些协程会触发 panic。
网友评论