Go 调度工作机制
下面是 Ian Lance Taylor (Go 核心开发者兼资深元老) 给出的答案:
我将给出从 Go 1.7 开始使用的调度器的概述.
调度器有三个基本结构, 称为 G, M 和 P. 一个 G 是一个 goroutine, 一个 M 是一个操作系统线程, 一个 P 是一个(逻辑)处理器.
调度器有确切的 GOMAXPROCS 数量的 P (GOMAXPROCS是一个环境变量和运行时函数, 用来设置程序中的并发度). 为了让 M 执行一个 G, 它必须获得一个P, 然后运行 G 直到停止. G 通过进行诸如 I/O 操作的系统调用, 阻塞一个 channel 操作, 调用 C 函数, 正在被预抢占(pre-emption)或其他一些小的情况来停止. 一个 G 只能在一个安全的地方被预抢占, 在当前的实现中只能在代码发生函数调用的时候发生.
当一个 G 被类似于一个 channel 操作那样阻塞时, 它将被放置在一个队列中, M 将寻找另一个可运行的 G . 如果没有可运行的 G, 则 M 将释放 P 并进入睡眠状态.
当 G 完成系统调用时, 必须重新获取 P. 如果没有 P 可用, 它将被标记为可运行, M 将进入睡眠状态.
当 channel 操作成功时, 它会唤醒另一个 goroutine, 将其标记为可运行, 并且如果有可用的 P, 则唤醒 M 来运行它.
虽然垃圾收集器大多是并发的, 但有几点要暂时停止所有的线程才能安全地转移到下一个收集阶段. 它通过标记所有正在运行的 goroutine 来预抢占. 当他们到达安全点时, G 和 M 将进入睡眠. 当垃圾收集器是唯一剩下的正在运行的 G 时, 它将移动到下一个阶段, 然后唤醒 GOMAXPROC 数量的 M, 它们将各自找到可运行的 G, 并继续下去.
runtime.Gosched 函数促使 M 将当前的 G 放在可运行的 goroutine 列表中, 并从该列表中选择一个新的 G 开始运行.