goroutine
进程:程序的执行过程。某程序在运行时的产物 线程:在用户进程空间中的控制流 一个进程一定有一个主线程,多线程需要通过主线程创建
运行时会自动帮我们创建和销毁系统级线程 goroutine 是用户级线程
GGGGGGG -> P -> M
每个goroutine都有独立的连续内存空间作为stack,调度某个goroutine执行时,只要把PC、SP、BP寄存器(分别对应代码入口地址、栈顶指针、栈基指针)设定到那个goroutine对应的地址就可以。这个操作完全是在用户空间执行的,不需要切换到内核空间,所以执行速度很快
用户级线程模型
操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程
很多的协程库会把自己一些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该KSE上运行,从而避免了内核调度器由于KSE阻塞而做上下文切换,这样整个进程也不会被阻塞了
内核级线程与用户级线程
用户级线程更加轻量,可以在进程中自己维护线程表,内核级线程的切换(休眠和重启)需要进行陷入,线程表在内核中维护,如果内核级线程阻塞,整个进程都会被阻塞
两级线程模型
可以动态绑定同一个KSE, 当某个KSE因为其绑定的线程的阻塞操作被内核调度出CPU时,其关联的进程中其余用户线程可以重新与其他KSE绑定运行
该模型为何被称为两级?即用户调度器实现用户线程到KSE的『调度』,内核调度器实现KSE到CPU上的『调度』。
G-P-M 模型概述
在Go语言中,每一个goroutine是一个独立的执行单元,相较于每个OS线程固定分配2M内存的模式,goroutine的栈采取了动态扩容方式, 初始时仅为8KB,随着任务执行按需增长,最大可达1GB(64位机器最大是1G,32位机器最大是256M),且完全由golang自己的调度器 Go Scheduler 来调度
M 代表一个内核线程,创建之初会被加入 M 全局列表 runtime.allm ,结构体内部会记录预关联的 P ,是否处于寻找 G 的自旋状态,起始运行函数等,还有 M 的空闲列表。一旦需要新启动或者恢复一个 M ,最初总是会处于自旋状态
P 也是类似的有全局和空闲列表,但是与 M 不同的是 P 有自己的状态 Pgcstop 是创建之初或者要进行短暂GC停顿时所处状态 Pidle 表明未与 M 进行绑定 Prunning 表示与某个 M 关联 Psyscall 表明其中运行的G正进行系统调用 Pdead 表明当前P不会再被使用,根据 MAXPROCS值进行多余的释放
调度
调度器工作时会维护两种用来保存G的任务队列:一种是一个Global任务队列,一种是每个P维护的Local任务队列
关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。为了运行goroutine,M需要持有(绑定)一个P,接着M会启动一个OS线程,循环从P的本地队列里取出一个goroutine并执行,P的队列为空的时候会首先尝试从全局队列寻找 G 来执行,运行完了之后 M 会解绑 P ,然后进入休眠
切换原则:
- 用户态阻塞/唤醒
- 系统调用:M 会变成可抢占,G完成系统调用后再标记成 runnable 回到 P 队列
工作共享:当一个处理器创建新的线程时,它试图将一部分线程迁移到其他的处理器上执行,期望更充分的利用那些 IDLE 状态的处理器。
工作窃取:未被充分利用的处理器会主动寻找其他处理器上的线程,并“窃取”一些线程
有时 M 与 G 会被成对地锁定在一起,可以说这个特性是为了 CGO 准备的,C的函数库会用到线程本地存储的一些技术,会把数据缓存在内核线程的私有缓存中,为防止数据丢失,调度器在为 M 匹配 G 的时候都会检查G是否被锁定,如果是当前 M 则继续执行,否则唤醒锁定的 M 继续执行G,然后接着为当前 M 分配其他的 G
M 在运行 G 之前还会检查是否有运行时串行任务等待执行,如果有(比如需要执行GC),那么则停止并阻塞当前 M 直至串行任务完成,完成后再唤醒 M 继续
主 goroutine
由 runtime.m0 负责运行,运行前创建一个特殊的 defer cleanup 函数用于退出时清理,然后启用GC标记清除的 goroutine 后,才执行 main 包中的 init 函数,执行 main 函数,执行结束后检查是否有 panic,有的话进行必要处理,最后结束主 goroutine 以及当前进程的执行
运行时相关的函数
runtime.GOMAXPROCS() Goexit() Gosched() NumGoroutine() 可用于简单地检查 G 引起的内存泄露 LockOSThread 和 UnlockOSThread,锁定当前的 G 和 M,前者多次调用只有最后一次生效 SetMaxStack() 用于约束单个 G 所能申请的栈空间的大小,在 init 调用前主 G 会对数值进行默认设置,32位和64位下分别为 250 M 和 1 G
协程池原理
Go调度器有一个复用机制,每次使用go关键字的时候它会检查当前结构体M中的P中,是否有可用的结构体G。如果有,则直接从中取一个,否则,需要分配一个新的结构体G。如果分配了新的G,需要将它挂到runtime的相关队列中
简单来说就是通过主动等待来让调度器有喘息的机会对 goroutine 进行复用 实现方式:将处理函数作为值的 channel ,开特定数量的 goroutine ,然后 for-range 接收函数并运行,通过将这个 chan 进行池化提高性能,来实现协程的复用