线程模型
内核级线程模型(KSE(Kernel Scheduling Entity))
关键点: 完全靠操作系统调度
每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做,应用程序对线程的创建、终止以及同步都基于内核提供的系统调用来完成
用户级线程模型
关键点: 完全靠自己调度
用户线程与内核线程KSE是多对一(N : 1)的映射模型,多个用户线程的一般从属于 单个进程
的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而无须借助系统调用来实现。操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程。
两级(混合型)线程模型
关键点: 自身调度与系统调度协同工作
用户线程与内核KSE是多对多(N : M)的映射模型:
首先,区别于用户级线程模型,两级线程模型中的一个进程可以与多个内核线程KSE关联,于是进程内的多个线程可以绑定不同的KSE,这点和内核级线程模型相似;
其次,又区别于内核级线程模型,它的进程里的所有线程并不与KSE一一绑定,而是可以动态绑定不同KSE, 当某个KSE因为其绑定的线程的阻塞操作被内核调度出CPU时,其关联的进程中其余用户线程可以重新与其他KSE绑定运行
GPM模型
基本概念
OS线程抽象,代表着真正执行计算的资源, 每一个 goroutine
实际上就是在 M
中执行, M
的数量目前最多 10000
个.
M
并不保存 G
的状态, 与 G
本身并没有关系, 所以 G
可以在不同的 M
执行
分配程序执行的上下文环境, 数量 <=
内核数量, 即同时能够并行执行的 G
的数量,相对于 G
而言, P
的角色相当于CPU.
程序代码中的每一次使用关键字 go
执行函数其实都生成了一个 G
,并将之加入到本地的 G
队列中, 之后 M
会生成 G
执行的上下文也就是绑定 P
来执行函数.
G
维护者goroutine需要的栈、程序计数器以及它所在的M等信息。
- Seched
代表着一个调度器 它维护有存储空闲的 M
队列和空闲的 P
队列,可运行的 G
队列,自由的 G
队列以及调度器的一些状态信息等。
模型调度
[图片上传失败...(image-401ce0-1543731894810)]
P
如何获得G
调度器Seched
生成一个M
, 然后M
需要持有(绑定)一个P
,接着M
会启动一个OS线程,循环让P
会首先从自己的本地队列(Local Quequ)中取可执行(Runnable)的G
执行, 如果本地队列中没有, 则会从全局队列(Globle Queue)中取G
, 如果还没有, 则会从其他的P
的本地队列中取一半的队列放入自己本地队列之中M
执行函数遇到阻塞,如何处理
实际代码执行中可能存在下面的问题,导致程序阻塞
<pre class="prettyprint hljs perl" style="box-sizing: border-box; overflow: auto; font-family: monospace, monospace; font-size: 10px; display: block; padding: 0.5em; color: rgb(171, 178, 191); background: rgb(40, 44, 52); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">blocking syscall (for example opening a file)
network input
channel operations
primitives in the sync package</pre>
主要可归为两类
- 用户态阻塞/唤醒
当 goroutine
因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个 wait
队列(如channel的waitq),该G的状态由 _Gruning
变为 _Gwaitting
,而M会跳过该G尝试获取并执行下一个G,如果此时没有runnable的G供M运行,那么M将解绑P,并进入 sleep
状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为runnable,尝试加入G2所在P的runnext,然后再是P的Local队列和Global队列。
- 系统调用阻塞
当G被阻塞在某个系统调用上时,此时G会阻塞在 _Gsyscall
状态,M也处于 block on syscall
状态,此时的M可被抢占调度:执行该G的M会与P解绑,而P则尝试与其它 idle
的M绑定,继续执行其它G。如果没有其它 idle
的M,但P的Local队列中仍然有G需要执行,则创建一个新的M;当系统调用完成后,G会重新尝试获取一个 idle
的P进入它的Local队列恢复执行,如果没有idle的P,G会被标记为runnable加入到Global队列。
调度使用了名叫 work stealing
的算法, 这种算法适用场景是任务之间的耗时相差比较大,即有的任务很耗时,有的任务很快完成,用这种用算法很合适;如果任务的耗时很平均则不适合,因为窃取任务也是需要抢占锁的,会造成额外的消耗。