Channel原理
上来先贴一段源码
type hchan struct {
qcount uint // 队列中的总元素个数
dataqsiz uint // 环形队列大小,即可存放元素的个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 //每个元素的大小
closed uint32 //标识关闭状态
elemtype *_type // 元素类型
sendx uint // 发送索引,元素写入时存放到队列中的位置
recvx uint // 接收索引,元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex //互斥锁,chan不允许并发读写
}
//recvq 那些已经准备好从 channel 中读取数据,但因为 channel 当前没有可供读取的数据而处于等待状态的 goroutines 的队列。
//sendq 等待机会将数据发送到 channel 中的 goroutine 的队列
向 channel 写数据:
若等待接收队列
recvq
不为空,则缓冲区中无数据或无缓冲区,将直接从recvq
取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。- 说明:如果接收队列
recvq
不为空,说明有 goroutine 正在等待从 channel 中读取数据。此时,如果 channel 的缓冲区中没有数据或者 channel 没有缓冲区,将直接从recvq
中取出一个等待的 goroutineG
,把要发送的数据写入G
,然后将G
唤醒,结束发送过程。
也就是说,原本写入的数据是写进 channel 的缓冲区中,但因为缓冲区没有数据或者channel没有缓冲区,导致这时,发送数据的操作不是将数据放入缓冲区,而是直接将数据传递给这个等待读取的 goroutine
G
。有人可能会有疑问,为什么channel有缓冲区,而且缓冲区没有数据,写入的数据不写入缓冲区而是写入等待读消息的goroutine G内。这是因为有等待读消息的goroutine G要的就是缓冲区的数据,但是由于缓冲区没有数据了,又有新数据来,不如直接将 数据 → 等待读消息的goroutine G,而不是多一步的 数据 → channel缓冲区 → 等待读消息的goroutine G。
这样做①是可以提高效率,直接将数据传递给等待读取的 goroutine 可以减少数据复制的开销,因为数据不需要先写入缓冲区再从缓冲区读取出来。这样可以提高程序的效率。②是可以避免死锁,如果缓冲区为空,而发送的数据被写入缓冲区,那么在没有其他 goroutine 准备读取的情况下,发送操作将无法完成,这可能导致死锁。通过直接将数据传递给等待读取的 goroutine,可以避免这种情况。
- 说明:如果接收队列
若等待接收队列 recvq 为空,且缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
若等待接收队列 recvq 不为空,且缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。
从 channel 读数据:
若等待发送队列
sendq
不为空,且没有缓冲区,直接从sendq
中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。- 说明:当从无缓冲的 channel 读取数据时,如果发送队列
sendq
不为空,这意味着有 goroutine 正在等待发送数据到这个变量。无缓冲的 channel 要求发送和接收操作同步进行,即发送方必须等待接收方准备好接收数据。
因为 channel 是无缓冲的,所以数据是在等待发送的 goroutine
G
的本地变量中。因为数据已经被接收方读取(也就是channel读取了)。
G
可以继续执行它之后的代码,通常是在G
中的send
操作之后的代码。- 说明:当从无缓冲的 channel 读取数据时,如果发送队列
在有缓冲区的情况下,若等待发送队列
sendq
不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。- 此情况基于有一个 goroutine 尝试从 channel 读取数据。
如果缓冲区中有数据,则从缓冲区取出数据(指有一个 goroutine 尝试从 channel 读取数据),结束读取过程。
- 这个操作不需要考虑
sendq
,因为缓冲区中已经有数据可以被读取。 - 图同2差不多
- 这个操作不需要考虑
将当前 goroutine 加入
recvq
,进入睡眠,等待被写 goroutine 唤醒。- 在这种情况下,因为没有数据可读且没有发送操作在等待(也就是没有缓冲区或者缓冲区内没有数据,同时
sendq
为空),执行读取操作的 goroutine 将自身加入到与 channel 关联的接收队列recvq
中。
- 在这种情况下,因为没有数据可读且没有发送操作在等待(也就是没有缓冲区或者缓冲区内没有数据,同时
上面的图多少有点误,比如说 两个队列里不为空的话其实都是有限个 goroutine
,我为了方便复制粘贴了几个空白在里面,想着意思到位就行。
其实这样看就会发现 channel
有点像科幻电影的能量传输仓,有一个三仓机器,左边一个仓右边一个仓,顶部可以按能量存储器(也就是缓冲区)就分别对应 channel
上面的四种情况。
关闭 channel:
1.关闭 channel 时会将 recvq
中的 G 全部唤醒,本该写入 G 的数据位置为 nil(因为要被读消息的对象没了)。将 sendq
中的 G 全部唤醒,但是这些 G 会 panic
(因为没有写消息的对象了)。
panic 出现的场景还有:
- 关闭值为 nil 的 channel
- 关闭已经关闭的 channel
- 向已经关闭的 channel 中写数据
备注:上面的data啊,变量啊,都是goroutine
里面的,channel
说白了就是用来在goroutine
里面传输数据的。
无缓冲 Chan 的发送和接收是否同步?
无缓冲的 channel:
- 当 channel 无缓冲时,发送操作必须等待另一个 goroutine 来接收数据。如果还没有接收方,发送方将会阻塞,直到有接收方准备好接收数据。
- 同样,接收操作也必须等待数据被发送。如果没有发送方,接收方将会阻塞,直到有发送方准备好发送数据。
- 因此,无缓冲的 channel 确保了发送和接收操作是同步的,即它们必须同时发生。
也就是说,无缓冲的 channel 通过阻塞强制让数据同步。
有缓冲的 channel:
- 当 channel 有缓冲时,发送操作可能不需要等待接收方。如果缓冲区未满,发送方可以直接将数据放入缓冲区,而不必立即等待接收方。
- 同样,接收操作也可能不需要等待发送方。如果缓冲区未空,接收方可以直接从缓冲区读取数据,而不必立即等待发送方。
- 然而,如果缓冲区满了,发送方在缓冲区有空间之前将会阻塞。类似地,如果缓冲区空了,接收方在缓冲区中有数据之前将会阻塞。
有缓冲的 channel 允许在缓冲区非空或未满的情况下发送和接收操作异步进行。
评论(0)