协程的状态

协程的生命周期

一个处于睡眠中的(通过调用time.Sleep)或者在等待系统调用返回的协程被认为是处于运行状态,而不是阻塞状态。

当一个新协程被创建的时候,它将自动进入运行状态,一个协程只能从运行状态而不能从阻塞状态退出。 如果因为某种原因而导致某个协程一直处于阻塞状态,则此协程将永远不会退出。 除了极个别的应用场景,在编程时我们应该尽量避免出现这样的情形。

一个处于阻塞状态的协程不会自发结束阻塞状态,它必须被另外一个协程通过某种并发同步方法来被动地结束阻塞状态。 如果一个运行中的程序当前所有的协程都出于阻塞状态,则这些协程将永远阻塞下去,程序将被视为死锁了。 当一个程序死锁后,官方标准编译器的处理是让这个程序崩溃。

协程的调度

并非所有处于运行状态的协程都在执行。在任一时刻,只能最多有和逻辑CPU数目一样多的协程在同时执行。 我们可以调用runtime.NumCPU 🗗 函数来查询当前程序可利用的逻辑CPU数目。 每个逻辑CPU在同一时刻只能最多执行一个协程。Go运行时(runtime)必须让逻辑CPU频繁地在不同的处于运行状态的协程之间切换,从而每个处于运行状态的协程都有机会得到执行。 这和操作系统执行系统线程的原理是一样的。

下面这张图显示了一个协程的更详细的生命周期。在此图中,运行状态被细分成了多个子状态。一个处于排队子状态的协程等待着进入执行子状态。一个处于执行子状态的协程在被执行一会儿(非常短的时间片)之后将进入排队子状态。

睡眠和等待系统调用返回子状态被认为是运行状态,而不是阻塞状态

标准编译器采纳了一种被称为M-P-G模型 🗗 的算法来实现协程调度。 其中,M表示系统线程,P表示逻辑处理器(并非上述的逻辑CPU),G表示协程。 大多数的调度工作是通过逻辑处理器(P)来完成的。 逻辑处理器像一个监工一样通过将不同的处于运行状态协程(G)交给不同的系统线程(M)来执行。 一个协程在同一时刻只能在一个系统线程中执行。一个执行中的协程运行片刻后将自发地脱离让出一个系统线程,从而使得其它处于等待子状态的协程得到执行机会

MPG模型

G: Goroutine,即我们在 Go 程序中使用 go 关键字创建的执行体;
M: Machine,或 worker thread,即传统意义上进程的线程;
P: Processor,即一种人为抽象的、用于执行 Go 代码被要求局部资源。只有当 M 与一个 P 关联后才能执行 Go 代码。除非 M 发生阻塞或在进行系统调用时间过长时,没有与之关联的 P。