理解channel 工作原理以及源码
Posted 张伯雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解channel 工作原理以及源码相关的知识,希望对你有一定的参考价值。
- goroutines: 独立执行每个任务,并可能并行执行
- channels: 用于 goroutines 之间的通讯、同步
- goroutine-safe,多个 goroutine 可以同时访问一个 channel 而不会出现竞争问题
- 可以用于在 goroutine 之间存储和传递值
- 其语义是先入先出(FIFO)
- 可以导致 goroutine 的 block 和 unblock
- 获取锁
enqueue(task0)
(这里是内存复制 task0)- 释放锁
- 获取锁
t = dequeue()
(同样,这里也是内存复制)- 释放锁
ch <- task1
ch <- task2
ch <- task3
M
: OS 线程G
: goroutineP
: 调度上下文P
拥有一个运行队列,里面是所有可以运行的 goroutine 及其上下文
G1
会调用运行时的gopark
,- 然后 Go 的运行时调度器就会接管
- 将
G1
的状态设置为waiting
- 断开
G1
和M
之间的关系(switch out),因此G1
脱离M
,换句话说,M
空闲了,可以安排别的任务了。 - 从
P
的运行队列中,取得一个可运行的 goroutineG
- 建立新的
G
和M
的关系(Switch in),因此G
就准备好运行了。 - 当调度器返回的时候,新的
G
就开始运行了,而G1
则不会运行,也就是 block 了。 G1
会给自己创建一个sudog
的变量- 然后追加到
sendq
的等候队列中,方便将来的 receiver 来使用这些信息恢复G1
。 G2
先执行dequeue()
从缓冲队列中取得task1
给t
G2
从sendq
中弹出一个等候发送的sudog
- 将弹出的
sudog
中的elem
的值enqueue()
到buf
中 - 将弹出的
sudog
中的 goroutine,也就是G1
,状态从waiting
改为runnable
- 然后,
G2
需要通知调度器G1
已经可以进行调度了,因此调用goready(G1)
。 - 调度器将
G1
的状态改为runnable
- 调度器将
G1
压入P
的运行队列,因此在将来的某个时刻调度的时候,G1
就会开始恢复运行。 - 返回到 G2
- 然后,
G2
给自己创建一个sudog
结构变量。其中g
是自己,也就是G2
,而elem
则指向t
- 将这个
sudog
变量压入recvq
等候接收队列 G2
需要告诉 goroutine,自己需要 pause 了,于是调用gopark(G2)
- 和之前一样,调度器将其
G2
的状态改为waiting
- 断开
G2
和M
的关系 - 从
P
的运行队列中取出一个 goroutine - 建立新的 goroutine 和
M
的关系 - 返回,开始继续运行新的
goroutine
- 和之前一样,调度器将其
goroutine-safe
hchan
中的lock mutex
存储、传递值,FIFO
- 通过
hchan
中的环形缓冲区来实现
- 通过
导致 goroutine 的阻塞和恢复
hchan
中的sendq
和recvq
,也就是sudog
结构的链表队列- 调用运行时调度器 (
gopark()
,goready()
) - 接收方阻塞 → 发送方直接写入接收方的栈
- 发送方阻塞 → 接受法直接从发送方的
sudog
中读取 - 先把所有需要操作的 channel 上锁
- 给自己创建一个
sudog
,然后添加到所有 channel 的sendq
或recvq
(取决于是发送还是接收) - 把所有的 channel 解锁,然后 pause 当前调用
select
的 goroutine(gopark()
) - 然后当有任意一个 channel 可用时,
select
的这个 goroutine 就会被调度执行。 - resuming mirrors the pause sequence
- 调用 Go 运行时调度器,这样可以保持 OS 线程不被阻塞
- 可以让 goroutine 醒来后不必获取锁
- 可以避免一些内存复制
无缓冲的 channel 行为就和前面说的直接发送的例子一样:
https://golang.org/src/runtime/select.go
更倾向于带锁的队列,而不是无锁的实现。
“性能提升不是凭空而来的,是随着复杂度增加而增加的。” - dvyokov
后者虽然性能可能会更好,但是这个优势,并不一定能够战胜随之而来的实现代码的复杂度所带来的劣势。
跨 goroutine 的栈读、写。
当然,任何优势都会有其代价。这里的代价是实现的复杂度,所以这里有更复杂的内存管理机制、垃圾回收以及栈收缩机制。
在这里性能的提高优势,要比复杂度的提高带来的劣势要大。
所以在 channel 实现的各种代码中,我们都可以见到这种 simplicity vs performance 的权衡后的结果。
|
回顾前面提到的 channel 的特性,特别是前两个。如果忽略内置的 channel,让你设计一个具有 goroutines-safe 并且可以用来存储、传递值的东西,你会怎么做?很多人可能觉得或许可以用一个带锁的队列来做。没错,事实上,channel 内部就是一个带锁的队列。 https://golang.org/src/runtime/chan.go
对于每一个 因为 为了方便描述,我们用
|
以上是关于理解channel 工作原理以及源码的主要内容,如果未能解决你的问题,请参考以下文章
深入理解View知识系列二- View底层工作原理以及View的绘制流程