Golang 基础 Part 5: Channel

文章讨论了 channel 在 Golang 中的基础知识、怎样使用 channelchannel的底层实现,以及使用 channel 的时候需要注意的地方。


channel 主要采用 CSP 并发模型实现的原理:不要通过共享内存来通信,而要通过通信来实现内存共享。它分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作 “同步模式”,带缓冲的则称为 “异步模式”。

1. 非缓冲的 channel

无缓冲的通道只有当发送方和接收方都准备好时才会传送数据, 否则准备好的一方将会被阻塞。

2. 带缓冲的 channel

有缓冲的 channel 区别在于只有当缓冲区被填满时, 才会阻塞发送者, 只有当缓冲区为空时才会阻塞接受者。值得注意的是,

  • 关闭 channel 以后仍然可以读取数据
  • for range 循环可以持续从一个 channel 中接收数据

3. channel 的底层实现

3.1 channel 底层结构体

Figure 1.channel 底层结构体
  • buf 是有缓冲的 channel 所特有的结构,用来存储缓存数据。是个循环链表
  • sendxrecvx 用于记录 buf 这个循环链表中的~发送或者接收的 ~index
  • lock 是个互斥锁。
  • recvqsendq 分别是接收 (<-channel) 或者发送 (channel <- xxx) 的 goroutine 抽象出来的结构体 (sudog) 的队列。是个双向链表

channel 的实现借助于结构体 hchan, 如下:

 1type hchan struct {
 2    qcount   uint           // total data in the queue
 3    dataqsiz uint           // size of the circular queue
 4    buf      unsafe.Pointer // points to an array of dataqsiz elements
 5    elemsize uint16
 6    closed   uint32
 7    elemtype *_type // element type
 8    sendx    uint   // send index
 9    recvx    uint   // receive index
10    recvq    waitq  // list of recv waiters
11    sendq    waitq  // list of send waiters
12
13    // lock protects all fields in hchan, as well as several
14    // fields in sudogs blocked on this channel.
15    //
16    // Do not change another G's status while holding this lock
17    // (in particular, do not ready a G), as this can deadlock
18    // with stack shrinking.
19    lock mutex
20}

3.2 send/recv 的细化操作

缓存链表中以上每一步的操作,都是需要加锁操作的!

每一步的操作的细节可以细化为:

  • 第一,加锁
  • 第二,把数据从 goroutine 中 copy 到“队列”中(或者从队列中 copy 到 goroutine 中)。
  • 第三,释放锁

goroutine 内存 copy 到 channel:

Figure 2. 内存条 copy 进 channel

channel 中的内存 copy 到 goroutine:

Figure 3. channel 内存 copy 到内存条

3.3. goroutine 的阻塞操作

goroutine 的阻塞操作,实际上是调用 send (ch <- xx) 或者 recv ( <-ch) 的时候主动触发的,

1//goroutine1 中,记做G1
2
3ch := make(chan int, 3)
4
5ch <- 1
6ch <- 1
7ch <- 1

channel 缓存满了以后,再次进行 send 操作 (ch<-1) 的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其他G去使用,

Figure 4. Goroutine 调度

同时G1也会被抽象成含有G1指针和 send 元素的 sudog 结构体保存到 hchansendq 中等待被唤醒。直到另一个 goroutine G2从缓存队列中取出数据,channel 会将等待队列中的G1推出,将G1当时 send 的数据推到缓存中,然后调用 Go 的 scheduler,唤醒G1,并把G1放到可运行的 goroutine 队列中。

翻译: