go routine与GMP模型

一.进程,线程,协程

进程:系统分配资源的最小单位,每个进程有自己的独立地址空间,进程间通信有不同的方式。

线程:进程中的执行单元,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

p结构体

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.全局队列饥饿:如果线程都被大型协程占据,导致全局队列中的协程处于饥饿状态,解决办法就是在本地小循环的时候以一定的概率从全局队列中拿去一定协程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乔可南-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值