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 uint32
elemtype *_type // 当前channel能够收发的元素类型
sendx uint // channel的发送操作处理到的位置
recvx uint // channel的接收操作处理到的位置
recvq waitq // 存储了当前channel由于缓冲区空间不足而阻塞的goroutine列表
sendq waitq
lock mutex
}
type waitq struct {
first *sudog
last *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 := <- a
fmt.Println("goroutine A received data: ", val)
return
}
func goroutineB(b <-chan int) {
val := <- b
fmt.Println("goroutine B received data: ", val)
return
}
func main() {
ch := make(chan int)
go goroutineA(ch)
go goroutineB(ch)
ch <- 3
time.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 <- ch
data, 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