深入浅出理解 Golang Channel
文章目录
序
channel 是 Go 语言中的一个非常重要的特性, 是 Go 在并发编程中与大多数语言不同的一点,上文谈到大多数语言在解决并发同步问题上采用的是 “共享内存模型” 来处理(go 也支持该模型,但推崇能够用 channel 解决优先使用 channel)而 Go 将 CSP (有关 CSP 的详细可在上文中详细了解)思想引入,使用 gorountine + channel 代替传统的 线程 + 共享内存的方式来处理并发。
上文简单聊到 channel 的应用场景以及简单示例,本文就 channel 详细谈谈 channel 的实现与应用。
什么是 channel
《Concurrency In Go》 中谈到
Goroutine 解放了程序员,让我们更能贴近业务去思考问题。而不用考虑各种像线程库、线程开销、线程调度等等这些繁琐的底层问题,goroutine 天生替你解决好了。
Channel 则天生就可以和其他 channel 组合。我们可以把收集各种子系统结果的 channel 输入到同一个 channel。Channel 还可以和 select, cancel, timeout 结合起来。而 mutex 就没有这些功能。
Go 的并发原则非常优秀,目标就是简单:尽量使用 channel;把 goroutine 当作免费的资源,随便用。
在 Go 中 goroutine 负责执行并发任务, 而 channel 用于 goroutine 之间的同步、通信。
对于熟悉操作系统的朋友可以将 channel 类比为 pipeline (管道,多个 channels 串联), 一个 Channel 在 gouroutine 间架起了一条管道,在管道里传输数据,实现 gouroutine 间的通信;由于它是线程安全的,所以用起来非常方便;channel 还提供“先进先出”的特性;它还能影响 goroutine 的阻塞和唤醒。
当然 channel 的底层实现就是通过 mutex (锁)实现控制的。只是 channel 是更高层次的并发编程原语,封装了更多的功能。至于 mutex 与 channel 两者的选择可以参考下图
channel 的使用
声明语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
chan T // 声明一个双向通道 chan<- T // 声明一个只能用于发送的通道 <-chan T // 声明一个只能用于接收的通道 // 非双向通道将在编译期检测,双向通道可以隐式转换为单向通道,反之不行 { bothway := make(chan int) // 定义一个双向类型为 int 的 chan out(bothway) } func out(out chan<- int) { // 隐式地转换为chan<- int x := 10 out <- x } |
channel 是一个引用类型,所以在初始化之前,值为 nil。channel 使用 make 函数进行初始化。可以向它传递一个 int 值,代表 channel 缓冲区的大小(容量),构造出来的是一个缓冲型的 channel;不传或传 0 的,构造的就是一个非缓冲型的 channel。
1 2 3 |
ch := make(chan T) // no buffer ch := make(chan T, 100) // use buffer len T[100] |
关闭 channel
1 2 3 |
ch := make(chan int) close(ch) // 通过 built-in 函数 close() 来关闭。 |
其中 * 重复关闭 channel 会导致 panic。 * 向关闭的 channel 发送数据会 panic。 * 从关闭的 channel 读数据不会 panic,读出 channel 中已有的数据之后再读就是 channel 类似的默认值,比如 chan int 类型的 channel 关闭之后读取到的值为 0。
channel 实现原理
对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数。
Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”,带缓冲的则称为“异步模式”。
同步模式,双方都需要同步就绪,只有在双方(gorutine) 同时处于 ready 状态下,数据在两者间传输(实质为 内存拷贝)。否则,任意一方先发起操作,都会被挂起,等待另一方出现唤醒。当通过一个无缓存 Channels 发送数据时,接收者收到数据发生在唤醒发送者 goroutine 之前(happens before)
Tips: 《Go 语言圣经》在讨论并发编程时,当我们说 x 事件在 y 事件之前发生(happens before),我们并不是说x事件在时间上比 y 时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。
1 2 3 4 5 6 7 8 9 |
func main() { done := make(chan struct{}) go func() { log.Println("done") // 此例中打印日志语句在 main goroutine 结束之前完成 done <- struct{}{} // signal the main goroutine (发送方) }() <-done // wait for background goroutine to finish (接收方) } |
异步模式,在缓冲槽可以使用下(接收方: 缓冲区有数据、 发送方: 缓存区还有容量),发送和接受方都可以顺利进行。否则,操作的一方同样被挂起,等待另一方唤醒。
数据结构
源码 runtime/hchan 1.12.x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex } |
Q & A
下面这段程序输出什么 ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
func main() { ch := make(chan int) // 无缓冲区 x := 10 ch <- x // 当前 goroutine 将立即进入 asleep 状态,发生死锁 fmt.Println(<-ch) } // log: fatal error: all goroutines are asleep - deadlock! func main() { ch := make(chan int, 1) // 带缓冲区 x := 10 ch <- x fmt.Println(<-ch) // 10 }