操作系统根据资源访问权限的不同,体系架构可以分为用户空间和内核空间;内核空间主要操作访问CPU资源,IO资源,内存资源等硬件资源,为应用程序提供最基本的基础资源;用户空间是上层应用程序的固定活动空间,用户空间不能直接访问内核空间,必须通过系统调用,库函数或者shell脚本来调用内核空间提供的资源。
线程模型的实现,可以分为以下几种方式:
1. 用户级线程模式
用户线程和内核线程KSE是 N:1 模型,N个用户空间线程在1个内核空间线程上运行,程序线程的创建、切换、同步、终止等线程工作必须自身来完成,无需借用系统调用来实现。一个进程中所有创建的线程都只和同一个KSE在运行时动态绑定,也就是说,操作系统只知道用户进程而对其中的线程是无感知的,内核所有的调度都基于用户进程。由于线程调度是在用户层面完成,不需要CPU在用户态和内核态切换;因此他的优势是上下文切换非常快,缺点是无法利用多核系统的优点。
2. 内核级线程模型
用户线程和内核线程KSE是 1:1 的模型,也就是每一个用户线程都绑定一个内核线程,而线程的调度都交给内核去做,应用程序对线程的创建、终止、同步等调度工作都是基于内核提供的系统调用来完成。这种模型的优势是实现简单,直接借助了操作系统内核的线程已经调度器,所以CPU可以快速切换调度线程,于是多个线程可以同时运行;缺点是由于借助了内核来完成线程的创建、销毁和线程之间的上下文切换和调度,因此资源成本大幅上涨,对性能影响很大。
3. 两级线程模型
用户线程和内核线程KSE是M:N的模型,两级线程模型中的一个进程可以与多个内核线程KSE相关联,于是进程中不同的线程可以绑定不同的KSE, 这点和内核级线程模型相似;其次,又区别于内核线程,他的进程中线程不是与KSE一一绑定,而是动态的绑定一个KSE;当某个KSE因为其绑定的线程的阻塞操作被内核调度出CPU时,其关联进程中的其余用户线程可以重新与其他KSE相绑定运行。所以,两级线程模型既不是用户级线程模型那种完全靠自身调度的模型也不是内核级线程模型那种完全靠操作系统调度的;go的两级模型即用户调度器实现用户线程到KSE的调度,内核调度器实现KSE到CPU的调度。
G-P-M模型
任何用户线程最终肯定要交由OS线程来执行,goruntine(G) 也同样,但是G并不直接交由OS线程运行,而是由 P - 逻辑处理器来作为两者之间的中介来运行。P可以看作一个抽象的资源或上下文,一个 P 绑定一个OS线程,在go中,把OS线程抽象为一个 M;G实际是由M通过P来调度运行的,但是对于G而言,P提供了G运行所需的一切资源和环境,在G看来,P就是CPU。由GPM这三种抽象出来的实现,最终形成了
GO调度器的基本结构:
G:表示 goruntine, 每个goruntine对应一个G的结构体,G存储goruntine的运行堆栈,状态及任务函数,可重用;每个G要被绑定到P上才能调度执行。
P:表示逻辑处理器,对每个G来说,P相当于CPU核,G只有被绑定到P上才能被调度。对M来说,P提供了相关的执行环境(context),如内存分配状态,任务队列等。
M:OS线程抽象,代表着真正执行计算的资源,在绑定了有效的P后,进入schedule循环;schedule循环的机制大致是从Global队列,P的local队列以及wait队列中获取到G, 切换到G的执行栈上并执行G的函数,调用 goexit做清理工作并回到M,如此循环。
G-P-M模型调度
Go调度器工作时会维护两种用于保存G的任务队列:一种是一个Global任务队列,一种是每一个P维护的任务队列。
当通过go关键字创建一个新的goruntine时,他会优先放入P的本地队列,当P本地队列满了,会把本地队列的一半送给全局队列。为了运行goruntine, M需要绑定一个P, 接着 M会启动一个 OS线程,循环从 P的本地队列中取出一个goruntine并执行。当M执行完当前P的local队列中所有的G后,会从global 队列中寻找G来执行【将全局G个数/P个数转移到本地队列】,如果global队列为空,他会随机挑选一个P,从中取出一半G到自己的队列中执行。
转自:https://www.cnblogs.com/williamjie/p/9267741.html