并行和并发
并发(concurrency)
两个或两个以上的任务在一段时间内被执行。例如跑步的时候,停下来系鞋带
并行(parallelism)
两个或两个以上的任务在同一时刻被同时执行。例如跑步的时候,边跑边听歌
线程模型
从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过系统调用、库函数或Shell脚本,来调用内核空间提供的资源。
现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。可以分为以下几种模型
用户级线程模型
多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。
内核级线程模型
直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。
两级线程模型
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。
Go语言的线程模型就是一种特殊的两极线程模型。我们称之为"MPG"模型
MPG模型
G
G是Goroutine的缩写,其实本质上也是一种轻量级的线程,相当于操作系统中的进程控制块,在这里就是Goroutine的控制结构,是对Goroutine的抽象。其中包括执行的函数指令及参数;G保存的任务对象;线程上下文切换,现场保护和现场恢复需要的寄存器(SP、IP)等信息。
M
M是称为Machine,一个M直接关联了一个内核线程。M是有线程栈的。如果不对该线程栈提供内存的话,系统会给该线程栈提供内存。M的PC寄存器指向G提供的函数,然后去执行。
P
P是一个抽象的概念,并不是真正的物理CPU。所以当P有任务时需要创建或者唤醒一个系统线程来执行它队列里的任务。所以P/M需要进行绑定,构成一个执行单元。
本地队列:当前P的队列,本地队列是Lock-Free,没有数据竞争问题,无需加锁处理,可以提升处理速度。
全局队列:全局队列为了保证多个P之间任务的平衡。所有M共享P全局队列,为保证数据竞争问题,需要加锁处理。相比本地队列处理速度要低于全局队列。
调度过程
首先创建一个G对象,G对象保存到P本地队列或者是全局队列。P此时去唤醒一个M。P继续执行它的执行序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行一个调度循环(调用G对象->执行->清理线程->继续找新的Goroutine执行)。
M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。
系统调用
均衡任务
G的创建流程
- 创建一个G对象,加入本地队列或全局队列
- 如果有空闲的P,则创建一个M
- M会启动一个底层线程,循环执行能找到的G任务
- G任务的执行顺序是,先从本地队列找,然后去全局队列找(一次拿全局G/P个数个任务),再之后去其他P中找(偷一半的任务)
M的创建过程
- 先找到一个空闲的P,如果没有则直接返回(保证了进程不会占用超过自己设定的cpu个数)
- 调用系统api创建线程,不同的操作系统,调用不一样
- 然后创建的这个线程里面才是真正做事的,循环执行G任务
如何终止长任务
- 启动的时候,专门创建一个线程sysmon,监控和管理任务
- 该线程记录所有P的G任务计数schedtick,该数值每执行一个G任务后递增
- 检测到某个schedtick一直没有递增,则说明这个P一直执行同一个G任务,则超过一段时间,就在这个G任务的栈信息中增加一个标记
- G任务在执行的时候,如果遇到非内联函数调用,则会检测一次上述标记,然后中断自己,把自己放到队列末尾,P就可以执行下一个G
G任务中断恢复
- 中断的时候将寄存器里的栈信息,保存到自己的G对象里面
- 当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了