简介
多进程、多线程已经提高了系统的并发能力,在高并发场景下,如果一个线程阻塞cpu,那就需要切换其他线程中去执行,为每个任务都创建一个线程成本比较高,因此又衍生出协程。我们知道一个线程分为“内核态“线程和”用户态“线程,goroutine是Go语言实现的用户态线程, 协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。
思考为什么协程和线程是M:N映射关系?
M个协程可以在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速,但是一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,无并发能力,因此需要N个线程
线程成本高主要表现在以下两个方面:
- 调度的高消耗CPU;操作系统线程的创建和切换都需要进入内核,都会占用很长时间,导致CPU有很大的一部分时间都被用来进行进程调度了
- 高内存占用;内核在创建操作系统线程时默认会为其分配一个较大的栈内存(进程虚拟内存会占用4GB[32位操作系统], 而线程也要大约4MB)
而相对的,用户态的goroutine则轻量得多:
- goroutine是用户态线程,创建和切换都在用户代码中完成而无需进入操作系统内核
- goroutine启动时默认栈大小只有2k,可自动调整容量
2.调度模型组成
1.被遗弃的GM模型
在12年的go1.1版本之前用的都是GM模型, G为Goroutine协程,M为Machine内核级线程, M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。
1.创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争
2.很差的局部性。比如当G中包含创建新协程的时候,M0创建了G1,因为将G1放入全局队列,,需要把G1交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M
模型组成
调度器主要由以下几部分组成:
schedt结构体:go调度器主体结构,保存调度器自身的状态信息,另外还有goroutine全局队列、空闲的工作线程M、空闲的p结构体对象等其他信息
// 重要的全局变量
var (
allgs []*g // 存储所有g的数组
allm *m // 存储所有m的链表,通过 链向下一个m
allp []*p // len(allp) == gomaxprocs
gomaxprocs int32 // 最大可创建p的个数,默认为cpu个数,可通过环境变量设置
ncpu int32 // cpu个数
m0 m // 代表进程的主线程
g0 g // m0的g0,也就是m0.g0 = &g0
sched schedt
)
type schedt struct {
// 空闲状态的m
midle muintptr // idle m's waiting for work
// 空闲状态的m个数
nmidle int32 // number of idle m's waiting for work
// m允许的最大个数
maxmcount int32 // maximum number of m's allowed (or die)
nmsys int32 // number of system m's not counted for deadlock
nmfreed int64 // cumulative number of freed m's
// 系统中goroutine的数目,会自动更新
ngsys uint32 // number of system goroutines; updated atomically
// 空闲的p列表
pidle puintptr // idle p's
// 有多少个状态为空闲的p
npidle uint32
// 有多少个m自旋
nmspinning uint32 // m处于自旋状态时,是当前m没有g可运行,正在寻找可运行的g
// Global runnable queue.
// 全局的可运行的g队列
runqhead guintptr
runqtail guintptr
// 全局队列的大小
runqsize int32
// freem is the list of m's waiting to be freed when their
// is set. Linked through .
freem *m
}
g的结构体: 它保存了goroutine的所有信息, 如保存调度信息
type g struct {
// 简单数据结构,lo 和 hi 成员描述了栈的下界和上界内存地址
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
// 当前的m
m *m // current m; offset known to arm liblink
// goroutine切换时,用于保存g的上下文,包括栈顶、栈低、pc等寄存器
sched gobuf
// 唯一的goroutine的ID
goid int64
// 标记是否可抢占
preempt bool
}
m结构体: Machine内核级线程, 每个工作线程都有唯一一个m结构体的实例对象与之对应,线程通过 ThreadLocal 存储各自的m对象;m结构体对象主要记录着工作线程的诸如栈的起止位置、当前正在执行的goroutine以及是否空闲等等状态信息之外,还通过指针维持着与p结构体的实例对象之间的绑定关系。
type m struct {
// 用来执行调度指令的 goroutine
g0 *g // goroutine with scheduling stack
morebuf gobuf // gobuf arg to morestack
// thread-local storage
tls [6]uintptr // thread-local storage (for x86 extern register)
mstartfn func()
// 当前运行的goroutine
curg *g // current running goroutine
// 关联p和执行的go代码
p puintptr // attached p for executing go code (nil if not executing go code)
nextp puintptr
id int64
// 用于标识睡眠还是唤醒
park note
// 是否自旋,自旋就表示M正在找G来运行
spinning bool // m is out of work and is actively looking for work
// m是否被阻塞
blocked bool // m is blocked on a note
// 用于链接allm
alllink *m // on allm
schedlink muintptr
//...
}
p结构体: P全称为Processor,所有的P都在程序启动时创建,每一个m都会与一个p结构体的实例对象关联在一起, p主要维护一份局部goroutine运行队列
type p struct {
// id也是allp的数组下标
id int32
status uint32 // one of pidle/prunning/...
// 单向链表,指向下一个P的地址
link puintptr
// 每调度一次加1
schedtick uint32 // incremented on every scheduler call
// 回链到关联的m
m muintptr // back-link to associated m (nil if idle)
// Queue of runnable goroutines. Accessed without lock.
// 可运行的goroutine的队列
runqhead uint32
runqtail uint32
runq [256]guintptr
// 下一个运行的g,优先级最高
runnext guintptr
}
3.浅析调度策略
- 我们通过 go func()来创建一个goroutine
- 有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中
- G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会向其他的MP组合偷取一个可执行的G来执行
- 一个M调度G执行的过程是一个循环机制,如下伪代码所示
// 程序启动时的初始化代码
......
for i := 0; i < N; i++ {
// 创建N个操作系统线程执行schedule函数
create_os_thread(schedule)