{% file src="../../.gitbook/assets/di-er-jiang-golang-xie-cheng-tiao-du-qi-yuan-li-yu-gmp-she-ji-si-xiang- (1).pdf" %}
- G代表一个goroutine,即我们要执行的具体任务
- P代表CPU的一个核,一个CPU的最大核数,即_GOMAXPROCS_限制了P的个数,也就设定最大并发数。
- M代表操作系统的线程,对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等。
而且他们三个的关系如下
- _G_需要绑定在_M_上才能运行;
- _M_需要绑定_P_才能运行;
- 程序中的多个_M_并不会同时都处于执行状态,最多只有_GOMAXPROCS_个_M_在执行。
通过引入_P_,实现了一种叫做_work-stealing_的调度算法:
- 每个_P_维护一个_G_队列;
- 当一个_G_被创建出来,或者变为可执行状态时,就把他放到_P_的可执行队列中;
- 当一个_G_执行结束时,P_会从队列中把该_G_取出;如果此时_P_的队列为空,即没有其他_G_可以执行, 就随机选择另外一个_P,从其可执行的_G_队列中偷取一半。
该算法避免了在Goroutine调度时使用全局锁。模型结构如下:
在M与P绑定后,M会不断从P的Local队列(runq)中取出G(无锁操作),切换到G的堆栈并执行,当P的Local队列中没有G时,再从Global队列中返回一个G(有锁操作,因此实际还会从Global队列批量转移一批G到P Local队列),当Global队列中也没有待运行的G时,则尝试从其它的P窃取(steal)部分G来执行。
当某个M陷入系统调用时,P则”抛妻弃子”,与M解绑,让阻塞的M和G等待被OS唤醒,而P则带着local queue剩下的G去找一个(或新建一个)idle的M,当阻塞的M被唤醒时,它会尝试给G找一个新的归宿(idle的P,或扔到global queue,等待被领养)。
转载自:Go in Action
如果创建一 个 goroutine 并准备运行,这个 goroutine 就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。
有时,正在运行的 goroutine 需要执行一个阻塞的系统调用,如打开一个文件。当这类调用 发生时,线程和 goroutine 会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。 与此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个 goroutine 来运行。一旦 被阻塞的系统调用执行完成并返回,对应的 goroutine 会放回到本地运行队列,而之前的线程会 保存好,以便之后可以继续使用。
如果一个 goroutine 需要做一个网络 I/O 调用,流程上会有些不一样。在这种情况下,goroutine 会和逻辑处理器分离,并移到集成了网络轮询器的运行时。一旦该轮询器指示某个网络读或者写 操作已经就绪,对应的 goroutine 就会重新分配到逻辑处理器上来完成操作。调度器对可以创建 的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建 10000 个线程。这个 限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。如果程序试图使用 更多的线程,就会崩溃。
并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处 理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做 了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的 总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学, 也是指导 Go 语言设计的哲学。
如果希望让 goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕 Go 语言运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。
下图展示了在一个逻辑处理器上并发运行goroutine和在两个逻辑处理器上并行运行两个并发的 goroutine 之间的区别。调度器包含一些聪明的算法,这些算法会随着 Go 语言的发布被更新和改进,所以不推荐盲目修改语言运行时对逻辑处理器的默认设置。如果真的认为修改逻辑处理 器的数量可以改进性能,也可以对语言运行时的参数进行细微调整。
基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时, 调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。
下图从逻辑处理器的角度展示了这一场景。在第 1 步,调度器开始运行 goroutine A,而 goroutine B 在运行队列里等待调度。之后,在第 2 步,调度器交换了 goroutine A 和 goroutine B。 由于 goroutine A 并没有完成工作,因此被放回到运行队列。之后,在第 3 步,goroutine B 完成 了它的工作并被系统销毁。这也让 goroutine A 继续之前的工作。
{% embed url="https://www.zhihu.com/question/20862617/answer/36191625" %}
{% embed url="https://morsmachine.dk/go-scheduler" %}
{% embed url="https://juejin.im/entry/6844903609813958669" %}
{% embed url="https://www.kancloud.cn/aceld/golang/1958305" %}