golang MPG并发模型

以上这张图就是golang的mpg模型中各个元素的说明:
-
M:物理线程,和其他语言中的线程是一致的;最大限制为10000个 -
P:逻辑处理器,负责调度协程;通常数量和CPU数量一致 -
G:即golang中通过go开启的协程
协程和线程的区别
协程被称作轻量级线程,在go语言中有几个优势:
- 协程栈初始大小为2k,远小于线程的1M;且协程栈可以动态扩容,最大到1G
- 协程的切片是逻辑控制器
P在语言级别(用户空间)实现的,相比于系统级的线程切换消耗少很多
协程的调度
正常情况下P会从自身的空闲队列中取出一个G来执行,在早期版本中golang实现的是非抢占式调用,只有遇到IO、管道、runtime.Gosched()等阻塞操作时才会进行切换
协程本身无法是无法自行进行切换的,在G遭遇到阻塞操作时,P会将当前的M脱离并同时绑定到一个新的线程M上,而原本的线程M则会继续阻塞在原本G的调用
除了和P绑定的线程外,其他的线程主要是就是用来处理被阻塞的任务上的
协程队列
go语言中有一个全局协程队列,使用go开启的新协程就会被放入这个队列中、阻塞的M执行完毕后也是将G放入到这个全局,P会定期从这里拉取新的G,
而每个P又会自己维护一个G队列,在消费掉自身的G后会先从全局队列中拉取;如果没有的话就从其他P的队列中偷取,每次偷一半
lua中的协程
lua中的协程和go语言的协程完全时不一样的,lua所有代码运行在一个线程中,实际上并不是并发的;
lua语言是不需要调度器P的,主要是协程内部主动调用函数切换,本质其实是类似于函数调用
抢占式调用
早期go语言实现的是非抢占式调用,这样的问题在于
for{}
如果只有一个P的情况下执行到上述代码,程序就会永远循环在这里,其他协程再也无法执行到
更严重的问题是是,go语言的垃圾回收是需要停止整个世界的,如果某个协程永远不停止,那么垃圾回收就会一致等待
但是如果是抢占式,那么就会在切换任务时,保存当前的上下文环境,因为当前线程如果正在做一件事,做到一半我们就强制停止,这时我们就必须多保存很多信息,避免再次切换回来时任务出错,这是需要付出代价的
go语言实现的抢占式调用是非常初级的,而且最终还是需要协程主动让出才能切换
什么时候需要抢占式调用
- 执行时间过长的协程:防止其他协程饿死
- GC需要停止某个协程来进行栈扫描
- GC需要STW停止
整个世界再进行工作
sysmon
在程序初始化的时候会创建一个后台线程执行sysmon,在程序执行期间每隔20us~10ms执行一次,对于执行超过10ms的协称会打上标记,供后续进行切换
初次之外sysmon还需要处理gc、网络轮询器的逻辑
协程切换
在go1.13版本前在如果sysmon发现需要进行调度会在函数的栈寄存器中打一个标记,这也就意味着for{}还是无法进行切换
在此之后是通过发送、监听sigPreempt信号实现的