不要通过共享内存的方式通信,而要通过通信的方式共享内存。

CSP模型

CSP(Communicating Sequential Proceses, 通信顺序进程),goroutine对应于CSP中的实体,channel对应于CSP中的信息传递媒介。

数据结构

  1. type hchan struct {
  2. qcount uint // channel中元素的个数
  3. dataqsiz uint // channel中的循环队列长度
  4. buf unsafe.Pointer // channel的缓冲区数据指针
  5. elemsize uint16 // 当前channel能够收发的元素大小
  6. closed uint32
  7. elemtype *_type // 当前channel能够收发的元素类型
  8. sendx uint // channel的发送操作处理到的位置
  9. recvx uint // channel的接收操作处理到的位置
  10. recvq waitq // 存储了当前channel由于缓冲区空间不足而阻塞的goroutine列表
  11. sendq waitq
  12. lock mutex
  13. }
  14. type waitq struct {
  15. first *sudog
  16. last *sudog
  17. }

可视化展示屏幕快照 2020-03-24 下午5.39.25.png

数据在channel中的传递

1. 创建channel

  1. // 无缓冲通道
  2. ch := make(chan int)
  3. // 有缓冲通道
  4. cch := make(chan int, 10)

上述过程创建出来的是一个指针*hchan,因此我们可以在函数间直接传递channel,而不用传递channel的指针。

2. 发送数据

向channel中发送数据,其实是对ring buff进行操作,即根据ring buff是否已满分为两种情况,向buffered channel发送数据,其实和向缓冲区已满的buffered channel发送数据,执行逻辑是一样的,那么向channel中发送数据,可以归类为缓冲区是否已满:

  • 缓冲区未满时

这个情况比较简单,数据以此存入缓冲区,同时sendx陆续加一。

屏幕快照 2020-03-24 下午5.52.06.png
~~

  • 缓冲区已满时

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

屏幕快照 2020-03-24 下午5.58.05.png

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

屏幕快照 2020-03-24 下午6.07.24.png

~~
举个例子:

  1. func goroutineA(a <-chan int) {
  2. val := <- a
  3. fmt.Println("goroutine A received data: ", val)
  4. return
  5. }
  6. func goroutineB(b <-chan int) {
  7. val := <- b
  8. fmt.Println("goroutine B received data: ", val)
  9. return
  10. }
  11. func main() {
  12. ch := make(chan int)
  13. go goroutineA(ch)
  14. go goroutineB(ch)
  15. ch <- 3
  16. time.Sleep(time.Second)
  17. }

~~

3. 接收数据

同样的,根据缓冲区是否被占满,从channel中读取数据也分为两种情况:

  • 3.1 缓冲区为空时

此时,接收数据的Goroutine被挂载到接收队列recvq上,此时该Goroutine休眠,等待对应的channel中有数据时,被调度器唤醒,然后读取数据。
当出现一个GoroutineG1向该channel中发送数据时,G1不会被挂在到sendq队列中,而是直接将发送的数据拷贝给G2
整个过程没有缓冲区的参与。

  • 3.2 缓冲区中有数据时

这时G2对channel先上锁,防止其他Goroutine消费该channel中的数据
然后G2从缓冲区中消费数据,从recvx指示的游标开始从缓冲区中读取数据,每次取一个数据,recvx值加一
拿到数据后,对该channel解锁。

屏幕快照 2020-03-24 下午6.37.30.png

从channel中接收数据有两种方式:

  1. data <- ch
  2. data, ok <- ch

其中,第二种写法用来判断当前channel是否被关闭
与发送过程对应,从channel中接收数据与hchan结构中等待发送队列和缓冲区中的数据有关

关闭一个channel

对该channel加锁,阻止其他Goroutine对该channel的缓冲区进行操作
将标志位closed置为1
将所有阻塞在sendq和recvq上的Goroutine转移到一个临时队列glist上,等待调度器继续处理
解锁该channel

屏幕快照 2020-03-24 下午6.49.01.png

  • 对于有缓冲的channel

是仍然可以从一个关闭的channel中读出数据的
由于关闭channel的时候,标志位closed被置为1,但是缓冲区中可能还存在数据,这时如果一个GoroutineG1从该channel中读取数据时,还是按照3.2的方式消费数据,直到缓冲区中的数据被消费完

  1. data, ok <- ch

直到ok值为false时,读出的数据才是无效的

死锁

channel的作用是在两个goroutine间进行信息传递,当接收方(发送方)缺席时,接收方(或发送方)操作channel时,就会造成死锁。
对于有缓冲的channel,当缓冲区被占满时,可以看做是一个无缓冲的channel

参考

Go语言设计与实现
Go-Questions-channel
channel&select源码分析
https://www.youtube.com/watch?v=d7fFCGGn0Wc