- goroutines: 独立执行每个任务,并可能并行执行
- channels: 用于 goroutines 之间的通讯、同步
- goroutine-safe,多个 goroutine 可以同时访问一个 channel 而不会出现竞争问题
- 可以用于在 goroutine 之间存储和传递值
- 其语义是先入先出(FIFO)
- 可以导致 goroutine 的 block 和 unblock
- 获取锁
-
enqueue(task0)(这里是内存复制 task0) - 释放锁
- 获取锁
-
t = dequeue()(同样,这里也是内存复制) - 释放锁
ch <- task1ch <- task2ch <- task3-
M: OS 线程 -
G: goroutine -
P: 调度上下文-
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 行为就和前面说的直接发送的例子一样:
- 先把所有需要操作的 channel 上锁
-
给自己创建一个
sudog,然后添加到所有 channel 的sendq或recvq(取决于是发送还是接收) -
把所有的 channel 解锁,然后 pause 当前调用
select的 goroutine(gopark()) -
然后当有任意一个 channel 可用时,
select的这个 goroutine 就会被调度执行。 - resuming mirrors the pause sequence
更倾向于带锁的队列,而不是无锁的实现。
“性能提升不是凭空而来的,是随着复杂度增加而增加的。” - dvyokov
后者虽然性能可能会更好,但是这个优势,并不一定能够战胜随之而来的实现代码的复杂度所带来的劣势。
- 调用 Go 运行时调度器,这样可以保持 OS 线程不被阻塞
跨 goroutine 的栈读、写。
- 可以让 goroutine 醒来后不必获取锁
- 可以避免一些内存复制
当然,任何优势都会有其代价。这里的代价是实现的复杂度,所以这里有更复杂的内存管理机制、垃圾回收以及栈收缩机制。
在这里性能的提高优势,要比复杂度的提高带来的劣势要大。
所以在 channel 实现的各种代码中,我们都可以见到这种 simplicity vs performance 的权衡后的结果。
-
|
回顾前面提到的 channel 的特性,特别是前两个。如果忽略内置的 channel,让你设计一个具有 goroutines-safe 并且可以用来存储、传递值的东西,你会怎么做?很多人可能觉得或许可以用一个带锁的队列来做。没错,事实上,channel 内部就是一个带锁的队列。 https://golang.org/src/runtime/chan.go
对于每一个 因为 为了方便描述,我们用
|