一.进程,线程,协程
进程:系统分配资源的最小单位,每个进程有自己的独立地址空间,进程间通信有不同的方式。
线程:进程中的执行单元,CPU调度的基本单位,共享进程的地址空间和系统资源。
线程有自己的栈和程序计数器,共享堆,所以线程之间通信比进程花销更小。
协程:轻量级线程,可以再一个线程中实现多个协程的切换,本质是包含运行状态的程序。
线程与协程比较:
1.一个线程可以拥有多个协程,与线程相比,协程不受操作系统的调度。
2.线程占用资源大,操作开销大,切换开销大,协程并发高,切换成本低(在用户态),并且协程之间的通信更加方便。
3.总体协程适用于高并发,高性能的场景。线程适用于需要os资源的场景。
二.协程(G)
1.协程的数据结构 源码runtime.go
type g struct {
stack stack // 协程栈
sched gobuf
param unsafe.Pointer
atomicstatus uint32 // 协程的状态
goid int64 // 协程的id
}
type stack struct {
lo uintptr // 协程栈的低地址
hi uintptr // 协程栈的高地址
}
type gobuf struct {
sp uintptr // 栈指针,现在运行到哪个方法
pc uintptr // 程序计数器,现在运行到哪个方法的哪一行
}
gobuf—结构体:sp(栈指针)管理现在用哪个栈,pc(程序计数器)管理现在到哪个代码。
stack:协程栈的高地址和低地址。
2.协程的执行过程:
go里面线程循环往复图左的方法,通过几个方法调用业务方法,业务方法用协程工作实现。
g0栈:g0协程在栈空间中分配的内存地址
1.程序启动的时候创建的栈,为运行runtime代码提供环境。
2.g0栈用于执行调度器的代码,用于记录函数调用跳转的信息。
线程循环具体执行过程:
1.通过schedule()跳转到这里,经过多次的跳转和调用,在队列中拿到协程(gc)。
2.跳转到execute(),在这时给拿到的gc进行赋值。
3.gogo方法使用汇编方法,拿到gobuf结构体(见上,有sp和pc),向协程栈人为插入了栈帧,最后返回调用gogo函数的位置,实现协程的切换。
4.开始执行业务方法,在g stack中进行,每个协程使用自己的协程栈,为每个协程保存自己的现场。
5.goexit()用于协程退出,状态设置为已完成。并且切换栈空间,切换到g0stack开始下一次循环。
部分源码如下
func schedule() {
...
var gp *g // 即将要运行的协程
var inheritTime bool
...
execute(gp, inheritTime)
}
三.GMP模型
问题出现:多线程并发时,会抢夺协程队列的全局锁——使用本地队列思想解决。
本地队列:指的是每个P(Processor)都有一个本地队列,里面存放了当前P需要运行的协程,这些协程是从全局队列中获取的。通过本地队列,可以减少对全局队列的竞争,提高并发效率。
1.m结构体简介
M:表示系统线程的抽象,拥有计算资源执行代码。G需要调度到M上才能执行,M才是真正工作的实体。
当M没有工作的时候,在休眠之前会自旋的寻找工作,检查全局队列,查看networkpoller或者进行工作窃取。
部分源码如下
//m代表工作线程,保存了自身使用的栈信息
type m struct {
// 记录工作线程使用的栈信息,在执行调度代码时候需要使用
// 执行用户goroutine代码的时候,使用用户自己的栈,所以调度的时候会发生栈的切换
g0 *g
morebuf gobuf
divmod uint32
_ uint32
·····
curg *g // 目前的goroutine对象
caughtsig guintptr // goroutine running during fatal signal
p puintptr // 当前工作线程绑定的p
nextp puintptr
oldp puintptr
id int64
···
spinning bool // m 已经处于自旋状态,从其他线程偷工作
blocked bool // m 被阻塞了
···
}
2.p结构体简介
P(Processor):处理器(送料器),本地队列,不断的送可运行的协程到工作线程上。
p里面有比如deferPool保存着defer语句,runq是本地可运行的协程队列等,
部分源码如下
type p struct {
id int32
status uint32 // one of pidle/prunning/...
···
m muintptr // back-link to associated m (nil if idle)
mcache *mcache
pcache pageCache
··
//deferpool 存储着程序中的defer语句
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
···
// 本地可运行的队列,可以不通过锁访问
runqhead uint32
runqtail uint32
runq [256]guintptr
3.GMP模型
G:goroutine协程
M:表示系统线程
P(Processor):处理器(送料器),本地队列,不断的送可运行的协程到工作线程上。
1.在GMP模型中,每个操作系统线程M都会有一个或多个处理器P,每个处理器P上会运行一到多个协程G。GMP模型通过本地队列来减少对全局队列的竞争,提高了协程的并发效率。
2.窃取式工作分配机制:如果本地没有协程G,则从全局协程队列里面拿一批协程,如果还没有就从其他的P上偷一些协程过来(任务窃取),增加了线程的利用率。
3.新建协程:随机寻找一个P,将新协程放入P的runnext(插队)
四.调度策略
协程饥饿问题:每个P维护着自己的本地协程队列,如果队列之中某些协程执行时间过长、阻塞时间过长、窃取策略不合理等多种因素,导致其余协程长时间等待得不到调度,就会出现饥饿问题。
解决办法:类似进程切换办法
1 占用时间过长的协程在执行中间保存现场,将执行信息保存在g结构体中,将协程放回全局队列,回到线程循环 的开始,执行新的协程。
切换时机:1.主动挂起gopark 2.系统调用完成时exitsyscall
2.抢占式调度:分配优先级,根据优先级,执行时间等因素切换协程。
3.全局队列饥饿:如果线程都被大型协程占据,导致全局队列中的协程处于饥饿状态,解决办法就是在本地小循环的时候以一定的概率从全局队列中拿去一定协程。