Channel 是 golang 并发编程中的一个重要的概念, 用于在不同的 goroutine 中传递信息以达到 communication, synchronization 的目的
- channel 是 groutine-safe 的
- channel 可在goroutine之间存储, 传递值
- channel 提供了 FIFO 的语意
- channel 可以导致 goroutine 阻塞或者唤醒阻塞
Make Channel
channel 分为 buffer channel, unbuffer channel
//unbuffered channel
unbufchan := make(ch int)
// buffered channel
bufchan := make(ch int, 3)
对于一个 buffered channel :ch := make(ch int, 3) 它底层分别由以下几个部分组成:
- buf 是一个循环队列, 用于存储 goroutine 向里写入的值
- lock锁用于在并发读写队列时保证数据安全
channel 数组结构分配在堆内存中, 当make操作后会返回一个 ch指针 指向其地址, 因此我们在函数,Goroutine间传递 ch 的时候, 不用传递 ch 的地址就可以达到共享内存的目的, 因为 ch 本身就是指针.
Communicate By Channel
接下来以下简单的代码片段为例子, 看下ch是这么被多个Goroutine利用的
// G1 sender 一系列的任务, G2工作goroutine接受任务处理
// sender G1
func main(){
// ...
for i:=0; i<times; i++{
// send task to ch
ch<-tasks[i]
}
}
// worker G2
func worker(){
for{
task := <- ch
// do task
doTask(task)
}
}
对于G1来说, 它需要往管道发送数据有如下几步:
- 获取管道中的锁
- 将 task0 复制到 管道的缓冲区
3. 释放锁
同样对于G2来说, 它从管道中取数据, 也需要如下几步
- 获取管道锁
2. 将缓冲区的数据 复制 到局部
3. 释放锁
由管道的内部锁保证了并发安全性, 通过复制变量的方式实现了 goroutine 通信
Do not communicate by sharing memory (excepy channel), instead, share memory(copies) by communicating
How Goroutine Block & Unblock
当channel的缓冲区满时, sender goroutine 会阻塞. 同时channel空时, receiver goroutine 会阻塞, 它是怎么做到的呢
首先分析下缓冲区满时, G1的行为
- 首先G1写入数据时发现缓冲区已满, runtime scheduler会将 G1 从线程M中下掉(OS 线程并没有阻塞), 阻塞的仅为goroutine G1. 同时分配其他 goroutine 在线线程运行
-
同时 channel 这时会用到 channel 中的两个结构
sendq,recvq. 分别用于存储此时被阻塞的 goroutine, 当G1被阻塞之前, 它会创建一个 sudog 结构体放在 sendq 阻塞队列中, 同于标示此时有发送goroutine因为缓冲区满了而阻塞// hchan 的运行是结构体 type hchan struct { // ... sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // ... } // type waitq struct { first *sudog last *sudog }
3. 当G2过来取缓存区的数据时, 会做如下几步
- 获取channel的锁
- dequeue 缓存区的数据使用
- dequeue sendq 队列中阻塞的 G1
- G2 会将 G1 发送的task04 放到buf缓存区, (没错就是G2将task04放到了buf中)
- G2 会将 G1 的状态设置为可运行的 (runable), 并将会在某一个时刻开始运行
关于是G2将task04放到buf区的动作, 其实也是一个 优化, 因为此时 G2已经获得了锁, 可以直接对 buf 区进行操作. 而不必使 G1 被唤醒后再去竞争锁.
当缓冲区空, ReceiverG2先到时的行为
- G2 从管道 buf 获取值发现其为空, 则被放到 recvq 队列, 然后阻塞
-
G1 往管道放数据时会做以下几步
- 获取管道的Lock
- dequque recvq 队列
- 直接将数据写到等待队列 G2 的内存区
t - 给 G2 设置 Runable 信号然后结束
这里的第三步也是一个优化动作, 它没有放到 buf 缓存区, 而是直接给到 recvq 队列 G2 的内存区, 从而避免 G2 被唤醒后再去竞争锁
值得注意的是, Golang 中一般是不允许 goroutine stacks 之间相互读写的, 但是 channel 这个 case 很特殊, 它允许 G1 直接将 task 写到 G2 的 stack 中, 从而优化性能