4
https://tonybai.com/2017/06/2...

线程模型

内核级线程模型(KSE(Kernel Scheduling Entity))

关键点: 完全靠操作系统调度
每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做,应用程序对线程的创建、终止以及同步都基于内核提供的系统调用来完成

用户级线程模型
关键点: 完全靠自己调度
用户线程与内核线程KSE是多对一(N : 1)的映射模型,多个用户线程的一般从属于单个进程的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而无须借助系统调用来实现。操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程。

两级(混合型)线程模型

关键点: 自身调度与系统调度协同工作
用户线程与内核KSE是多对多(N : M)的映射模型:
首先,区别于用户级线程模型,两级线程模型中的一个进程可以与多个内核线程KSE关联,于是进程内的多个线程可以绑定不同的KSE,这点和内核级线程模型相似;
其次,又区别于内核级线程模型,它的进程里的所有线程并不与KSE一一绑定,而是可以动态绑定不同KSE, 当某个KSE因为其绑定的线程的阻塞操作被内核调度出CPU时,其关联的进程中其余用户线程可以重新与其他KSE绑定运行

GPM模型

基本概念

  • M

OS线程抽象,代表着真正执行计算的资源, 每一个goroutine实际上就是在M中执行,M的数量目前最多10000个.
M并不保存G的状态, 与G本身并没有关系, 所以G可以在不同的M执行

  • P

分配程序执行的上下文环境, 数量<=内核数量, 即同时能够并行执行的G的数量,相对于G而言, P的角色相当于CPU.

  • G

程序代码中的每一次使用关键字go执行函数其实都生成了一个G,并将之加入到本地的G队列中, 之后M会生成G执行的上下文也就是绑定P来执行函数.
G维护者goroutine需要的栈、程序计数器以及它所在的M等信息。

  • Seched

代表着一个调度器 它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。

模型调度

关于线程说明:
内核级线程: 线程的切换由内核控制,可以直接用户态到内核态,或者内核态到用户态,能够充分利用CPU的核数
用户级线程: 线程的切换由用户自己控制,不需要内核控制,少了进出内核的消耗,但是不能很好的利用CPU的核数

用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。

1.P如何获得G
调度器Seched生成一个M, 然后M需要持有(绑定)一个P,接着M会启动一个OS线程,循环让P会首先从自己的本地队列(Local Quequ)中取可执行(Runnable)的G执行, 如果本地队列中没有, 则会从全局队列(Globle Queue)中取G, 如果还没有, 则会从其他的P的本地队列中取一半的队列放入自己本地队列之中

2.M执行函数遇到阻塞,如何处理
实际代码执行中可能存在下面的问题,导致程序阻塞

blocking syscall (for example opening a file)
network input
channel operations
primitives in the sync package

主要可归为两类

  • 用户态阻塞/唤醒

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的算法, 这种算法适用场景是任务之间的耗时相差比较大,即有的任务很耗时,有的任务很快完成,用这种用算法很合适;如果任务的耗时很平均则不适合,因为窃取任务也是需要抢占锁的,会造成额外的消耗。

参考文档


pengj
270 声望6 粉丝