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
这个循环链表中的~发送或者接收的~index
lock
是个互斥锁。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 队列中。