前言
⼀切软件都运⾏在操作系统上, 使这些软件能够运⾏且⼯作起来, 真正⽤来计算的是CPU。 早期的操作系统的每个程序就是⼀个进程, 直到⼀个程序运⾏完, 才能执⾏下⼀个进程, 就是单进程时代, 所有的程序只能以串⾏的⽅式执⾏

早期的单进程操作系统, ⾯临两个问题。
- 单⼀的执⾏流程。 计算机只能⼀个任务⼀个任务处理, 所有的程序⼏乎是阻塞的, 更不⽤说具备图形化界⾯或者⿏标这种异步交互的处理能⼒。
- 进程阻塞所带来的CPU时间浪费。 在⼀个进程完整的⽣命周期中, 所要访问的物理部分包括CPU、 Cache、 主内存、 磁盘、 ⽹络等, 不同的硬件媒介处理计算的能⼒相差甚⼤。 如果将这些处理速度不同的处理媒介通过⼀个进程串在⼀起, 则会出现⾼速度媒介等待和浪费的现象。 如当⼀个程序加载⼀个磁盘数据的时候, 在读写的过程中, CPU处于等待状态, 那么对于单进程的操作系统来讲, 很明显会造成CPU运算能⼒的浪费, 因为CPU此刻本应该被合理地分配到其他进程上去做⾼层的计算。
多进程/ 多线程时代的调度器需求
多进程/ 多线程的操作系统解决了阻塞的问题, ⼀个进程阻塞CPU可以⽴刻切换到其他进程中去执⾏, ⽽且调度CPU的算法可以保证在运⾏的进程都可以被分配到CPU的运⾏时间⽚。 从宏观来看, 似乎多个进程是在同时被运⾏

但是如果满⾜宏观上每个进程/线程是⼀起执⾏的, 则CPU必须切换, 每个进程会被分配到⼀个**时间⽚**中去执行。这会导致新的问题出现,进程拥有太多的资源, 进程的创建、 切换、 销毁都会占⽤很⻓的时间, CPU虽然利⽤起来了, 但如果进程过多, CPU会有很⼤的⼀部分被⽤来进⾏进程切换调度。

对于Linux操作系统来⾔, CPU对进程和线程的态度是⼀样的, 如果系统的CPU数量过少,⽽进程/ 线程数量⽐较庞⼤, 则相互切换的频率也就会很⾼, 其中中间的切换成本越来越⼤。 这⼀部分的性能消耗实际上是没有做在对程序有⽤的计算算⼒上, 所以尽管线程看起来很美好, 但实际上多线程开发设计会变得更加复杂, 开发者要考虑很多同步竞争的问题, 如锁、 资源竞争、 同步冲突等。
多进程、 多线程已经提⾼了系统的并发能⼒, 但是在当今互联⽹⾼并发场景下, 为每个任务都创建⼀个线程是不现实的, 因为这样就会出现极⼤量的线 程同时运⾏, 不仅切换频率⾼, 也会消耗⼤量的内存: 进程虚拟内存会占⽤4GB(32位操作系统,寻址空间为2^32) , ⽽线程也要⼤约4MB。 ⼤量的进程或线程出现了以下两个新的问题。 (1)⾼内存占⽤。 (2)调度的⾼消耗CPU。
协程
⼯程师发现其实可以把⼀个线程分为“内核态”和“⽤户态”两种形态的线程。 所谓⽤户态线程就是把内核态的线程在⽤户态实现了⼀遍⽽已, ⽬的是更轻量化(更少的内存占⽤、 更少的隔离、 更快的调度) 和更⾼的可控性(可以⾃⼰控制调度器) 。 ⽤户态中的所有东⻄内核态都看得⻅, 只是对于内核⽽⾔⽤户态线程只是⼀堆内存数据⽽已。
⼀个⽤户态线程必须绑定⼀个内核态线程, 但是CPU并不知道有⽤户态线程的存在, 它只知道它运⾏的是⼀个内核态线程(Linux的PCB进程控制块)

如果将线程再进⾏细化, 内核线程依然叫线程(Thread), ⽽⽤户线程则叫协程(Coroutine)。 操作系统层⾯的线程就是所谓的内核态线程, ⽤户态线程则多种多样, 只要能满⾜在同⼀个内核线程上执⾏多个任务, 例如Co-routine、 Go的Goroutine、 C#的Task等。
既然⼀个协程可以绑定⼀个线程, 那么能不能多个协程绑定⼀个或者多个线程呢? 答案是可以的, 它们分别是N: 1关系、 1: 1关系和M: N关系。
N:1关系
N个协程绑定1个线程, 优点就是协程在⽤户态线程即完成切换, 不会陷⼊内核态, 这种切换⾮常轻量快速, 但缺点也很明显, 1个进程的所有协程都绑定在1个线程上

N:1关系⾯临的⼏个问题如下:
- 某个程序⽤不了硬件的多核加速能⼒。
- 某⼀个协程阻塞, 会造成线程阻塞, 本进程的其他协程都⽆法执⾏了, 进⽽导致没有任何 并发能⼒。
1:1关系
1个协程绑定1个线程, 这种⽅式最容易实现。 协程的调度都由CPU完成了, 虽然不存在N:1的缺点, 但是协程的创建、 删除和切换的代价都由CPU完成, 成本和代价略显昂贵。

M:N关系
M个协程绑定1个线程, 是N:1和1:1类型的结合, 克服了以上两种模型的缺点, 但实现起来最为复杂, 如图1.7所示。 同⼀个调度器上挂载M个协程, 调度器下游则是多个CPU核⼼资源。协程跟线程是有区别的, 线程由CPU调度是抢占式的, 协程由⽤户态调度是协作式的, ⼀个协程让出CPU后, 才执⾏下⼀个协程, 所以针对M:N模型的中间层的调度器设计就变得尤为重要, 提⾼线程和协程的绑定关系和执⾏效率也变为不同语⾔在设计调度器时的优先⽬标。

Golang “调度器” 的由来
Go语⾔为了提供更容易使⽤的并发⽅法, 使⽤了Goroutine和Channel。 Goroutine来⾃协程的概念, 让⼀组可复⽤的函数运⾏在⼀组线程之上, 即使有协程阻塞, 该线程的其他协程也可以被runtime调度, 从⽽转移到其他可运⾏的线程上。 最关键的是, 程序员看不到这些底层的细节, 这就降低了编程的难度, 提供了更容易的并发。在Go语⾔中, 协程被称为Goroutine, 它⾮常轻量, ⼀个Goroutine只占⼏KB, 并且这⼏KB就⾜够Goroutine运⾏完, 这就能在有限的内存空间内⽀持⼤量Goroutine, 从⽽⽀持更多的并发。 虽然⼀个Goroutine的栈只占⼏KB, 但实际是可伸缩的, 如果需要更多内存, 则runtime会⾃动为Goroutine分配。Goroutine的特点, 占⽤内存更⼩(⼏KB) 和调度更灵活(runtime调度) 。
Go目前使用的调度器是2012年重新设计的,因为之前的调度器性能存在问题,所以使用4年就被废弃了,那么我们先来分析一下被废弃的调度器是如何运作的?
大部分文章都是会用G来表示Goroutine,用M来表示线程,那么我们也会用这种表达的对应关系。

下面我们来看看被废弃的golang调度器是如何实现的?

M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。
老调度器有几个缺点:
- 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
- M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M'。
- 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
GMP设计思想
⾯对之前调度器的问题, Go设计了新的调度器。 在新调度器中, 除了M(线程) 和G(协程) , ⼜引进了P(处理器)

处理器包含了运⾏Goroutine的资源, 如果线程想运⾏Goroutine, 必须先获取P, P中还包含了可运⾏的G队列。
GPM模型
在Go中, 线程是运⾏Goroutine的实体, 调度器的功能是把可运⾏的Goroutine分配到⼯作线程上。在GPM模型中有以下⼏个重要的概念

全局队列(Global Queue): 存放等待运⾏的G。 全局队列可能被任意的P去获取⾥⾯的G,所以全局队列相当于整个模型中的全局资源, 那么⾃然对于队列的读写操作是要加⼊互斥动作的。
P的本地队列: 同全局队列类似, 存放的也是等待运⾏的G, 但存放的数量有限, 不超过256个。 新建G′时, G′优先加⼊P的本地队列, 如果队列满了, 则会把本地队列中**⼀半**[^1]的G移动到全局队列。
P列表: 所有的P都在程序启动时创建, 并保存在数组中, 最多有GOMAXPROCS(可配置) 个。
M: 线程想运⾏任务就得获取P, 从P的本地队列获取G, 当P队列为空时, M也会尝试从全局队列获得**⼀批**[^2]G放到P的本地队列, 如果全局队列为空则从其他P的本地队列“偷”⼀半放到⾃ ⼰P的本地队列。 M运⾏G, G执⾏之后, M会从P获取下⼀个G, 不断重复下去。
Goroutine调度器和OS调度器是通过M结合起来的, 每个M都代表了1个内核线程, OS调度器负责把内核线程分配到CPU的核上执⾏。
P和M的个数问题
P的数量
- 由启动时环境变量
$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。
- 由启动时环境变量
M的数量
go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。
runtime/debug中的SetMaxThreads函数,设置M的最大数量
一个M阻塞了,会创建新的M。
M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。
P和M何时会被创建
- P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
- M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
调度器的设计策略
策略⼀: 复⽤线程
避免频繁地创建、 销毁线程, ⽽是对线程的复⽤。
偷取(Work Stealing)机制
当本线程⽆可运⾏的G时, 尝试从其他线程绑定的P偷取G, ⽽不是销毁线程

这⾥需要注意的是, 偷取的动作⼀定是由P发起的, ⽽⾮M, 因为P的数量是固定的, 如果⼀个M得不到⼀个P, 那么这个M是没有执⾏的本地队列的, 更谈不上向其他的P队列偷取了。
移交(Hand Off)机制
当本线程因为G进⾏系统调⽤阻塞时, 线程会释放绑定的P, 把P转移给其他空闲的线程执 ⾏

此时若在M1的GPM组合中, G1正在被调度, 并且已经发⽣了阻塞, 则这个时候就会触发移交的设计机制。 GPM模型为了更⼤程度地利⽤M和P的性能, 不会让⼀个P永远被⼀个阻塞的G1耽误之后的⼯作, 所以遇⻅这种情况的时候, 移交机制的设计理念是应该⽴刻将此时的P释放出来。

为了释放P, 所以将P和M1、 G1分离, M1由于正在执⾏当前的G1, 全部的程序栈空间均在M1中保存, 所以M1此时应该与G1⼀同进⼊阻塞的状态, 但是已经被释放的P需要跟另⼀个M进⾏绑定, 所以就会选择⼀个M3(如果此时没有M3, 则会创建⼀个新的或者唤醒⼀个正在睡眠的M) 进⾏绑定, 这样新的P就会继续⼯作, 接收新的G或者从其他的队列中实施偷取机制。
策略⼆: 利⽤并⾏
GOMAXPROCS设置P的数量, 最多有GOMAXPROCS个线程分布在多个CPU上同时运⾏。GOMAXPROCS也限制了并发的程度, 例如GOMAXPROCS=核数/2, 表示最多利⽤⼀半的CPU核进⾏并⾏。
策略三: 抢占
在Co-routine中要等待⼀个协程主动让出CPU才执⾏下⼀个协程, 在Go中, ⼀个Goroutine最多占⽤CPU 10ms, 防⽌其他Goroutine⽆资源可⽤, 这就是Goroutine不同于Co-routine的⼀个地⽅

策略四: 全局G队列
在新的调度器中依然有全局G队列,当P的本地队列为空时,优先从全局队列获取,如果全局队列为空时则通过work stealing机制从其他P的本地队列偷取G。

go func() 调度流程

从上图我们可以分析出几个结论:
- 我们通过 go func()来创建一个goroutine;
- 有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;
- G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会从全局或者想其他的MP组合偷取可执行的G来执行;
- 一个M调度G执行的过程是一个循环机制;
- 当M执行某一个G时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P;
- 当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。[^3]
调度器的生命周期

特殊的M0和G0
M0
(1)启动程序后的编号为0的主线程。 (2)在全局命令runtime.m0中, 不需要在heap堆上分配。 (3)负责执⾏初始化操作和启动第1个G。 (4)启动第1个G后, M0就和其他的M⼀样了。
G0
(1)每次启动⼀个M, 创建的第1个Goroutine就是G0。 (2)G0仅⽤于负责调度G。 (3)G0不指向任何可执⾏的函数。 (4)每个M都会有⼀个⾃ ⼰的G0。 (5)在调度或系统调度时, 会使⽤M切换到G0, 再通过G0调度。 (6)M0的G0会放在全局空间。
Go调度器调度场景过程全解析
下面将列举一些场景,这些场景⼏乎能够涵盖G在调度的过程中所遇⻅到的情况, 通过对这些调度场景的分析也能够帮助我们更好地体会Go语⾔调度器的魅⼒。
场景1: G1创建G3
本场景主要体现GPM调度的局部性, P拥有G1, M1获取P后开始运⾏G1。当G1使⽤go func( )创建了G3, 为了局部性, G3优先加⼊P1的本地队列。

场景1主要体现了GPM调度器的局部性, 默认规定, 如果⼀个G1创建⼀个新的G3, 则这个G3会优先放在G1所在的本地队列中。 这是由于G1和G3所保存的内存和堆栈信息最为相同, 它们⽬前所在的M1和P对于G1和G3的切换成本⾮常⼩, 这也是局部性要保证的特点。
场景2:G1执⾏完毕
G1运行完成后(函数:goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:schedule)。从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute)。实现了线程M1的复用。[^4]

场景2主要体现了GPM调度模型对M的复⽤性, 当⼀个G已经执⾏完毕, 不管其他P中有没有空闲的, M⼀定会优先从⾃⼰绑定的P中的本地队列获取待调⽤的G来执⾏, 这⾥待调⽤的G就是G2。
场景3:G2开辟过多的G
假设每个P的本地队列只能保存4个G, ⽽此时的P的本地队列是空的。 若G2要创建6个G, 则此时G2只能够创建前4个G(G3、 G4、 G5、 G6)放在G2当前的队列中, 多余的G将不会添加到P1的本地队列中。

如果⼀个G创建了过多的G, 则本地队列会出现放满的现象, 所以多余的G需要按照场景4的逻辑进⾏安排。
场景4:G2本地满再创建G7
G2在创建G7的时候, 发现P1的本地队列已满, 需要执⾏负载均衡算法, 把P1中本地队列中前⼀半的G, 还有新创建的G转移到全局队列

将G3、 G4和刚刚创建的G7⼀起放到了全局队列, ⽽将P1中本地队列中的G5和G6移动到队列的头部。这些G被转移到全局队列时, 会被打乱顺序[^5], 所以G3、 G4、 G7是以乱序的⽅式移到全局队列。
场景5: G2本地未满再创建G8
G2创建G8时, P1的本地队列未满, 所以G8会被加⼊P1的本地队列

新创建的G会优先放到本地的队列中, 也是由于局部性质导致。 由于本地队列还有其他G在队列的头部, 所以新创建的G8会依次从队列尾部进⼊, 当G2调度完成, 下⼀个被调度的应该是G5。
场景6:唤醒正在休眠的M
在GPM模型中, 在创建⼀个G的时候, 运⾏的G会尝试唤醒其他空闲的P和M组合去执⾏。 含义是, 有可能之前有过剩的M, 这些M不会⽴刻被操作系统回收, ⽽是会放在⼀个休眠线程队列。 触发这个M从休眠队列唤醒的条件就是在尝试创建⼀个G的时候。

⽬前只有⼀个P1和M1在绑定
- 并且P1正在执⾏G2。当G2尝试创建⼀个新的G的
- 就会触发尝试从休眠线程队列获取M, 并且尝试去绑定新的P及P的本地队列。
- M2如果发现⽬ 前可以有被利⽤的P资源, 则M2就会被激活, 并且绑定到P2上
此时M2和P2若绑定, 就需要寻找其他的G去执⾏, 每个M都会有⼀个调度其他G的G0, 所以⽬ 前M2和P2在没有正常的G可⽤的时候, G0会被P调度。 如果P的本地队列为空, 并且P正在调度G0, 则M2、 P2、 G0组合就被称为⼀个⾃旋线程
场景7:被唤醒的M2从全局队列批量取G
M2尝试从全局队列(简称GQ) 取⼀批G放到P2的本地队列(简称LQ) 。 M2从全局队列取的G数量符合下⾯的公式: $$ n=min(len(GQ)/GOMAXPROCS + 1, cap(LQ)/2) $$ 相关源码:
// 从全局队列中偷取,调用时必须锁住调度器
func globrunqget(_p_ *p, max int32) *g {
// 如果全局队列中没有 g 直接返回
if sched.runqsize == 0 {
return nil
}
// per-P 的部分,如果只有一个 P 的全部取
n := sched.runqsize/gomaxprocs + 1
if n > sched.runqsize {
n = sched.runqsize
}
// 不能超过取的最大个数
if max > 0 && n > max {
n = max
}
// 计算能不能在本地队列中放下 n 个
if n > int32(len(_p_.runq))/2 {
n = int32(len(_p_.runq)) / 2
}
// 修改本地队列的剩余空间
sched.runqsize -= n
// 拿到全局队列队头 g
gp := sched.runq.pop()
// 计数
n--
// 继续取剩下的 n-1 个全局队列放入本地队列
for ; n > 0; n-- {
gp1 := sched.runq.pop()
runqput(_p_, gp1, false)
}
return gp
}⾄少从全局队列取1个G, 但每次不要从全局队列将太多的G移动到P本地队列, 给其他P留⼀部分。 这是从全局队列到P本地队列的负载均衡

如果此时的M2为⾃ 旋线程状态, 全局队列的数量为3, 并且P2的本地队列容量为4, 则通过负载均衡公式得到, ⼀次从全局队列获取G的个数为1。 M2就会从全局队列的头部获取G3加⼊P2的本地队列中。 G3被加⼊本地队列之后, 就需要被G0调度, G0也就被替换为G3, 并且与此同时全局队列中的G7和G4会依次向队列的头部移动。⼀旦G3被调度起来, M2就不再是⼀个⾃旋线程了。
场景8:M2从M1中偷取
假设G2⼀直在M1上运⾏, 经过两轮后, M2已经把G7、 G4从全局队列获取了P2的本地队列并完成运⾏, 全局队列和P2的本地队列将变为空。

M2⼜会处于调度G0的状态, 此时的M2处于⾃ 旋线程状态。 处于⾃ 旋状态的MP组合会不断地寻找可以调度的G, 否则在这空等待就是在浪费线程资源。
全局队列已经没有G, 所以M就要执⾏偷取: 从其他有G的P那⾥偷取⼀半G过来, 放到⾃ ⼰的P本地队列。 P2从P1的本地队列尾部取⼀半的G。本例中⼀半则只有1个G8, 所以M2就会尝试将G8偷取过来, 放到P2的本地队列。
场景9:自旋线程的最大限制
M1本地队列G5、 G6已经被M2偷⾛并运⾏完成, 当前M1和M2分别在运⾏G2和G8。 GPM模型中GOMAXPROCS变量⽤于确定P的数量, 在GPM中P的数量是固定的, ⼀旦确定了P的数量之后, P的数量就不可以动态添加或者删减了。

M3和M4没有Goroutine也可以运⾏, 所以⽬前绑定的都是各⾃的G0, M3和M4处于⾃旋状态, 它们不断地寻找Goroutine。
为什么要让M3和M4⾃旋? ⾃旋本质上是在运⾏, 线程在运⾏却没有执⾏G, 就变成了浪费CPU。 为什么不销毁来节约CPU资源? 因为创建和销毁CPU也会浪费时间, Go调度器GPM模型的思想是当有新Goroutine创建时, ⽴刻能有M运⾏它, 如果销毁再新建就增加了时延, 也降低了效率。 当然也考虑了过多的⾃旋线程也会浪费CPU, 所以系统中最多有GOMAXPROCS个⾃旋的线程(当前例⼦中的GOMAXPROCS=4, 所以⼀共有4个P) , 多余的没事做的线程则会休眠。 如果现在有⼀个M5正在运⾏, 但是已经没有多余的P可以和它绑定, 则M5就会被放到休眠线程队列
场景10:G发生阻塞的系统调用
假定当前除了M3和M4为⾃旋线程, 还有M5和M6为空闲的线程(没有得到P的绑定, 注意这⾥最多只能有4个P, 所以P的数量应该永远是M≥P) , G8创建了G9。
若此时G8进⾏了阻塞的系统调⽤, 则M2和P2⽴即解绑, P2会执⾏以下判断, 如果P2本地队列有G或全局队列有G需要被执⾏, 并且有空闲的M, 则P2会⽴即唤醒1个M和它绑定(如果没有休眠的M, 则会创建⼀个M进⾏绑定) , 否则P2会加⼊空闲P列表, 等待M获取可⽤的P。
本场景中, P2本地队列有G9, 可以和其他空闲的线程M5绑定。 M2在G8阻塞的期间, G8会临时占⽤M的资源。
以上便是⼀个G发⽣系统阻塞时的场景。

场景11:G发生非阻塞的系统调用
接着场景10讲解, G8创建了G9, 假如G8之前的系统调⽤结束, ⽬前是⼀个⾮阻塞状态。
在场景10中, 虽然M2和P2会解绑, 但M2会记住P2, 然后G8和M2进⼊系统调⽤状态。 当G8和M2退出系统调⽤时, M2会尝试获取之前具有绑定关系的P2, 如果P2可以被获取, 则M2将和P2重新绑定, 如果⽆法获取M2, 则会进⾏其他⽅式处理。

在当前的范例中, 此时的P2正在和M5绑定, 所以M2⾃然也就获取不到P2。 M2会尝试从空闲P队列寻找可⽤的P。
如果依然没有空闲的P在队列中, 则M2就等于找不到可⽤的P与⾃⼰进⾏绑定, G8会被记为可运⾏状态, 并加⼊全局队列, M2因为没有P的绑定⽽变成休眠状态(⻓时间休眠等待会被GC回收销毁)。
小结
本文引自Golang的协程调度器原理及GMP设计思想并将自己和网友评论的一些内容整理,供后续自己或他人理解golang的GMP模型
脚注
[^1]:由于数据局部性,新创建的 G 优先放入本地队列,在本地队列满了时,会将本地队列的一半 G 和新创建的 G 打乱顺序,一起放入全局队列;本地队列如果一直没有满,也不用担心,全局队列的 G 永远会有 1/61 的机会被获取到,调度循环中,优先从本地队列获取 G 执行,不过每隔61次,就会直接从全局队列获取,至于为啥是 61 次,Dmitry 的视频讲解了,就是要一个既不大又不小的数,而且不能跟其他的常见的2的幂次方的数如 64 或 48 重合; [^2]: 有个计算方法,3个数,1.p本地队列容量的一半,即:256/2=128;2.全局队列长度/gomaxprocs+1;3.全局队列长度;这三个取最小的作为拿的g的数量。 [^3]:系统调用前M会保存当前的p为oldP,系统调用结束后:优先尝试绑定oldP。如果oldP绑定失败则从全局P队列获取一个可用的P成功则运行g;失败则把g放入全局队列, M进入沉睡 [^4]: 当一个 goroutine 执行完毕后,它的栈空间会被回收,但是它的 Goroutine 结构体不会被销毁。相反,它会被放入一个全局的 Goroutine 队列中,等待被调度器回收。调度器会负责管理这些 Goroutine,包括重用它们的栈空间、重新分配它们的 CPU 时间片等等。因此,当一个 goroutine 执行完毕后,并不会立即被销毁,而是被放入全局队列中等待回收。 [^5]: 防止某些 goroutine 长时间等待调度的情况发生,从而避免了某些 goroutine 长期被饿死的情况,保证了调度的公平性
参考链接
https://www.bilibili.com/video/BV19r4y1w7Nx
https://www.yuque.com/aceld/golang/srxd6d
https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/runtime/proc.go;bpv=0