CSP模型
CSP(Communicating Sequential Proceses, 通信顺序进程),goroutine对应于CSP中的实体,channel对应于CSP中的信息传递媒介。
数据结构
type hchan struct {qcount uint // channel中元素的个数dataqsiz uint // channel中的循环队列长度buf unsafe.Pointer // channel的缓冲区数据指针elemsize uint16 // 当前channel能够收发的元素大小closed uint32elemtype *_type // 当前channel能够收发的元素类型sendx uint // channel的发送操作处理到的位置recvx uint // channel的接收操作处理到的位置recvq waitq // 存储了当前channel由于缓冲区空间不足而阻塞的goroutine列表sendq waitqlock mutex}type waitq struct {first *sudoglast *sudog}
可视化展示
数据在channel中的传递
1. 创建channel
// 无缓冲通道ch := make(chan int)// 有缓冲通道cch := make(chan int, 10)
上述过程创建出来的是一个指针*hchan,因此我们可以在函数间直接传递channel,而不用传递channel的指针。
2. 发送数据
向channel中发送数据,其实是对ring buff进行操作,即根据ring buff是否已满分为两种情况,向buffered channel发送数据,其实和向缓冲区已满的buffered channel发送数据,执行逻辑是一样的,那么向channel中发送数据,可以归类为缓冲区是否已满:
- 缓冲区未满时
这个情况比较简单,数据以此存入缓冲区,同时sendx陆续加一。

~~
- 缓冲区已满时
这时,buffered channel 和 unbuffered channel可以等同为同一种channel
此时,发送方会被挂载到sendq,如下图的G1,该Goroutine被阻塞,防止调度器对该Goroutine的调度,等待缓冲区中有位置时才被接触阻塞。

此时,如果另一个GoroutineG2从该channel中消费一个数据时,被阻塞在sendq上的G1会先出队(即该Goroutine被唤醒),channel缓冲区recvx指向的索引位置的数据白拷贝给G2,同时直接将G1待发送的数据拷贝至recvx指向的索引位置(由于缓冲区是ring buffer)

~~
举个例子:
func goroutineA(a <-chan int) {val := <- afmt.Println("goroutine A received data: ", val)return}func goroutineB(b <-chan int) {val := <- bfmt.Println("goroutine B received data: ", val)return}func main() {ch := make(chan int)go goroutineA(ch)go goroutineB(ch)ch <- 3time.Sleep(time.Second)}
3. 接收数据
同样的,根据缓冲区是否被占满,从channel中读取数据也分为两种情况:
- 3.1 缓冲区为空时
此时,接收数据的Goroutine被挂载到接收队列recvq上,此时该Goroutine休眠,等待对应的channel中有数据时,被调度器唤醒,然后读取数据。
当出现一个GoroutineG1向该channel中发送数据时,G1不会被挂在到sendq队列中,而是直接将发送的数据拷贝给G2。
整个过程没有缓冲区的参与。
- 3.2 缓冲区中有数据时
这时G2对channel先上锁,防止其他Goroutine消费该channel中的数据
然后G2从缓冲区中消费数据,从recvx指示的游标开始从缓冲区中读取数据,每次取一个数据,recvx值加一
拿到数据后,对该channel解锁。

从channel中接收数据有两种方式:
data <- chdata, ok <- ch
其中,第二种写法用来判断当前channel是否被关闭
与发送过程对应,从channel中接收数据与hchan结构中等待发送队列和缓冲区中的数据有关
关闭一个channel
对该channel加锁,阻止其他Goroutine对该channel的缓冲区进行操作
将标志位closed置为1
将所有阻塞在sendq和recvq上的Goroutine转移到一个临时队列glist上,等待调度器继续处理
解锁该channel

- 对于有缓冲的channel
是仍然可以从一个关闭的channel中读出数据的
由于关闭channel的时候,标志位closed被置为1,但是缓冲区中可能还存在数据,这时如果一个GoroutineG1从该channel中读取数据时,还是按照3.2的方式消费数据,直到缓冲区中的数据被消费完
data, ok <- ch
直到ok值为false时,读出的数据才是无效的
死锁
channel的作用是在两个goroutine间进行信息传递,当接收方(发送方)缺席时,接收方(或发送方)操作channel时,就会造成死锁。
对于有缓冲的channel,当缓冲区被占满时,可以看做是一个无缓冲的channel
参考
Go语言设计与实现
Go-Questions-channel
channel&select源码分析
https://www.youtube.com/watch?v=d7fFCGGn0Wc
