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