介绍
Golang 语言社区流传一句口号:
Do not communicate by sharing memory; instead, share memory by communicating.
安全访问共享变量是并发编程的一个难点,在 Golang 语言中,倡导通过通信共享内存,实际上就是使用 channel 传递共享变量,在任何给定时间,只有一个 goroutine 可以访问该变量的值,从而避免发生数据竞争。
关于 channel 的使用方法,我们在之前的文章中「Go 语言学习之 goroutine 和 channel」介绍过,本文我们从 channel 的数据结构和执行逻辑两个方面介绍一下它的实现原理。
数据结构
我们看一下 Golang 源码中 channel 的数据结构。
$GOROOT/src/runtime/chan.go
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters lock mutex }
阅读 channel 的源码,可以发现 channel 的数据结构是 hchan 结构体,包含以下字段:
- qcount 当前队列中剩余的元素个数
- datasize 环形队列的长度
- buf 环形队列的指针
- elemsize 元素的大小
- closed 关闭标识
- elemtype 元素的类型
- sendx 发送索引位置
- recvx 接收索引位置
- recvq 等待接收的协程队列
- sendq 等待发送的协程队列
- lock 互斥锁
通过阅读 channel 的数据结构,可以发现 channel 是使用环形队列作为 channel 的缓冲区,datasize
环形队列的长度是在创建 channel 时指定的,通过 sendx
和 recvx
两个字段分别表示环形队列的队尾和队首,其中,sendx 表示数据写入的位置,recvx 表示数据读取的位置。
字段 recvq 和 sendq 分别表示等待接收的协程队列和等待发送的协程队列,当 channel 缓冲区为空或无缓冲区时,当前协程会被阻塞,分别加入到 recvq 和 sendq 协程队列中,等待其它协程操作 channel 时被唤醒。其中,读阻塞的协程被写协程唤醒,写阻塞的协程被读协程唤醒。
字段 elemtype 和 elemsize 表示 channel 中元素的类型和大小,需要注意的是,一个 channel 只能传递一种类型的值,如果需要传递任意类型的数据,可以使用 interface{}
类型。
字段 lock 是保证同一时间只有一个协程读写 channel。
03
执行逻辑
在 Golang 语言中,可以对 channel 进行读写操作,本小节我们分别介绍一下 channel 的读操作和写操作。
写操作 channel,分为两种情况,第一种是 channel 的缓冲区未写满,直接将数据写入缓冲区,结束 send 操作;第二种是 channel 的缓冲区已写满,此时,当前操作 channel 的协程将会被加入 sendq 等待发送的协程队列,等待被读协程唤醒。
需要注意的是,当 recvq 队列不为空时,证明缓冲区没有数据,但是有协程等待读取数据,此时,数据将不再写入缓冲区,而是会直接把数据传递给 recvq 队列中的第一个协程。
读操作 channel,也分为两种情况,第一种是 channel 的缓冲区中有数据,直接读取缓冲区中的数据,结束 recv 操作;第二种是 channel 的缓冲区中没有数据,此时,当前操作 channel 的协程加入 recvq 等待接收的协程队列,等待被写协程唤醒。
需要注意的是,当 sendq 队列不为空时,并且缓冲区已写满,此时,将直接从 sendq 队列中的第一个协程读取数据。
当 channel 被关闭时,recvq 和 sendq 中的所有协程被唤醒,其中 recvq 中的协程读取到的数据全部是 nil,sendq 中的协程会触发 panic。
04
总结
本文我们在 channel 的数据结构和执行逻辑两个方面介绍了 channel 的实现原理,其中,执行逻辑小节中,重点介绍了 channel 的读写操作。如果读者朋友们想要了解创建 channel 的执行逻辑,可以阅读源码中的函数 func makechan(t *chantype, size int) *hchan
。
推荐阅读:
参考资料:
https://golang.org/doc/effective_go#sharing
https://blog.golang.org/codelab-share