golang MPG并发模型

golang MPG并发模型

mpg并发模型图.png

以上这张图就是golangmpg模型中各个元素的说明:

  • 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信号实现的

你可能感兴趣的:(golang MPG并发模型)