偶然看到一条关于goroutine有趣的QA:
https://news.ycombinator.com/item?id=12459841
发现可以以另一种方式来理解goroutine,欢迎拍砖。
关键概念说明:
M: machine, M对应于内核线程;
P: processor,P是一种在M上运行的context,维护了goroutine的列表;
G: goroutine核心结构,维护了goroutine需要的栈、程序计数器以及所在的M等信息;
假设一开始没有M和P,没有sysmon,同时GOMAXPROCS=1
这个时候go程序以单线程模式运行,
每次执行一个G,无法做到抢占,而必须由程序自己yield来让出cpu,
让出cpu时当前G需要将相关上下文信息保存到自己的结构中,
然后程序查找下一个等待的G并执行。
遇到systemcall时,当前线程会新开一个线程用以接管执行之后的G,
而当前线程会阻塞知道系统调用返回,
之后将返回结果保存到某个地方,
最后该线程退出。
加入M,减少线程创建/销毁消耗,GOMAXPROCS=1不变
上面的主要问题是单线程执行任务,
遇到syscall便会在创建/销毁线程操作中消耗很多资源,
所以这时我们想复用线程。
这里我们加入了一定数量的M,每个M对应一个内核线程,
由于GOMAXPROCS=1,同时在跑的M只有一个,其他的在sleep。
与上面有所改进的是,
如果当前M遇到syscall,它不用新创建一个线程,
而是唤醒M列表中的某个M来接管执行之后的G就可以了,
而自己负责等待系统调用,
系统调用返回后将结果写到某个地方便sleep及等待唤醒。
加入调度锁,支持多线程并发,GOMAXPROCS>1
上面的场景还是同时只有一个线程在运行,
我们想利用多核同时跑几个线程,
那应该怎么做呢?
我们加入一个叫调度锁的东西,
用于解决多线程从G列表争抢资源G发生冲突的场景,
设置GOMAXPROCS>1,这时可以最多同时跑GOMAXPROCS个线程。
调度锁是一个全局锁,如果资源上锁了,那所有线程都必须等待。
这时M数量>=GOMAXPROCS,同时在跑的M数量==GOMAXPROCS。
加入P,提高并发性能
上面使用了调度锁,虽然解决了多线程并发问题,
但是由于是对列表G全局加锁,并发性能并不好。
这时我们加入了P,将结构关系G:M变为了G:P:M。
每一个M对应一个P, 而每个P维护了一个G列表,
这时每新建一个G,会按某种顺序加到某个P的G队列末尾。
这个时候不再用全局锁了,M也不用每次到全局G队列中争抢G,
只需要从P的G队列中拿出一个就绪的G运行即可。
同样如果一个M进入syscall,
它会释放P以允许其他M来接管自己的P及P所维护的G队列,
自己等待系统调用返回并保存结果后,
进入M的空闲队列并等待唤醒。
加入sysmon,实现任务的夺取
像上面提到的,当一个M进入syscall而释放P时具体怎么实现呢?
这里引入了sysmon这个在Go程序启动时就会创建的一个独立线程,
它用于任务的监控和管理, 内部就是一个无限循环,
它发现如果有M处于syscall状态时便将它的P及对P上的G队列抢过来,
放到其他可用的M上运行(有可能需要新建M, 也可能是唤醒某个sleep的M)。
至此,很粗略的概括了goroutine的协作式调度原理。
原文链接:https://www.f2er.com/go/187727.html