Golang 基础 Part 5: Channel
文章讨论了 channel 在 Golang 中的基础知识、怎样使用 channel、channel的底层实现,以及使用 channel 的时候需要注意的地方。
channel 主要采用 CSP 并发模型实现的原理:不要通过共享内存来通信,而要通过通信来实现内存共享。它分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作 “同步模式”,带缓冲的则称为 “异步模式”。
1. 非缓冲的 channel
无缓冲的通道只有当发送方和接收方都准备好时才会传送数据, 否则准备好的一方将会被阻塞。
2. 带缓冲的 channel
有缓冲的 channel 区别在于只有当缓冲区被填满时, 才会阻塞发送者, 只有当缓冲区为空时才会阻塞接受者。值得注意的是,
- 关闭
channel以后仍然可以读取数据 for range循环可以持续从一个channel中接收数据
3. channel 的底层实现
3.1 channel 底层结构体
buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表sendx和recvx用于记录buf这个循环链表中的~发送或者接收的~indexlock是个互斥锁。recvq和sendq分别是接收 (<-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:
channel 中的内存 copy 到 goroutine:
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去使用,
同时G1也会被抽象成含有G1指针和 send 元素的 sudog 结构体保存到 hchan 的 sendq 中等待被唤醒。直到另一个 goroutine G2从缓存队列中取出数据,channel 会将等待队列中的G1推出,将G1当时 send 的数据推到缓存中,然后调用 Go 的 scheduler,唤醒G1,并把G1放到可运行的 goroutine 队列中。