线程调度模型
有三大线程调度模型,其主要差异在于用户级线程(即协程)与KSE(Kernel Scheduling Entity 内核调度实体)之间的对应关系
内核级线程模型
用户级线程与内核级线程1:1对应,线程创建,切换,销毁均需内核参与
优点:
- 多处理器环境中,内核能并行执行同一进程的多个线程
- 同一进程中的某个线程阻塞不会导致进程被阻塞,可以继续执行同进程下别的线程
- 内核在某进程的线程阻塞时可切换执行另一进程的线程
缺点:
- 线程创建,切换,销毁成本高
用户级线程模型
用户级线程与内核级线程N:1对应,线程创建,切换,销毁等操作均在用户态完成(内核无法感知)
优点:
- 线程创建,切换,销毁等管理代价相比前者小得多
- 线程能利用的表空间和堆栈空间相比前者更多[相较前者更自由,不受内核固定栈大小限制(内核线程一般固定 8MB)]且可动态增长
缺点:
- 进程同一时间只能有一个线程运行(因为内核无法感知,并不清楚有多线程存在),单一线程阻塞会导致整个进程阻塞
- 多处理机情况下,进程也只能在单个处理机下时分复用(只对应1个KSE,退化为按进程分配时间片)
两级线程模型
用户级线程与内核级线程N
该模型充分吸收前两者模型的优点,并尽量避免缺点,实现较为复杂
GMP(Goroutine-Machine-Processor)
首先明确,内核不会自行将线程置于某种状态,内核只会根据程序发起的系统调用将线程置于对应状态,并且给处于runnable状态的线程分配CPU时间片
G即Goroutine,Go语言的轻量级线程,属于两级线程模型
M实际上即系统线程,与KSE(Kernel Scheduling Entity 内核调度实体)一一对应
KSE 是可被操作系统内核调度的对象实体,是最小调度单元
P即Processor,指逻辑处理器(即资源,但不等同于CPU逻辑核心,为runtime调度资源),其内部关联了本地G队列(LRQ),最多可存放256个G
GMP相互关系为:G在P中排队等待执行,M需要先持有P才能被分配CPU时间片实际运行
此处不讨论G0(G0属于M,是特殊的Goroutine)
GMP调度流程大致为:
- M抢得(持有)一个P
- M从P的本地队列(LRQ)中得到一个等待被执行的G
-
- 若P中无待执行的G,则从全局G队列中取得一批G放入LRQ中
-
- 若全局队列中也无待执行G,M进入sleep
- M得到CPU时间片,开始执行G
-
- 若执行时遇到阻塞
-
-
- 若为syscall阻塞,M进入阻塞状态,解除与P的关联,G阻塞结束重新进入某个P中等待执行,M阻塞结束后重新抢夺P后继续下一步
-
-
-
- 若为netpoller等异步挂起(用户态阻塞)状态,G被放入netpoll阻塞表等待回调,M不阻塞,继续下一步
-
- G执行完毕,M重新获取LRQ中的下一个G,重复以上操作
特殊情况:
M0,即启动程序后创建的编号0的主线程,M0上的实例放置在全局变量(runtime.m0)中,不在heap上分配,M0特殊仅因为其执行了启动和初始化流程,以上流程结束后M0与其他普通M等价,并没有特权
但M0仍具有特殊性,即永不销毁(除非程序结束运行),通常也承担绑定 signal-handling thread(处理同步信号)
G0,即每启动一个M会自动创建的第一个G,G0属于M而不属于任何一个P,G0的不指向可执行函数,而是在M进行调度时使用(使用G0的栈空间)
G-M-P的数量:
- G的数量可由runtime.NumGoroutine()获得,无数量限制
- M的数量由语言本省动态控制,原则为保证每一个P可以被分配到一个可用的M(可用M = P),M会在不足时自动创建并在长时间空闲时(由 sysmon 触发)退出,M有理论数量上限(10000)
- P的数量一般自动设置为=逻辑处理器数量,也可通过环境变量$GOMAXPROCS设置
GMP调度模型的优势:
- 实现了M的高效复用,减少了系统线程创建与销毁的资源消耗
- 通过handoff与work stealing,实现了M的高效复用与负载均衡
- G可在不同的M中执行,且切换代价极低
- M通过在与自身关联的P中获取G,实现了lock free,减少了锁开销