看到有一篇写得很清楚的博客,做个笔记。
有时候G需要调用一些无法避免阻塞的原生代码, 这时M会释放持有的P并进入阻塞状态, 其他M会取得这个P并继续运行队列中的G.
因为同一时间只有一个线程(M)可以拥有P, P中的数据都是锁自由(lock free)的, 读写这些数据的效率会非常的高.
自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量.
本地运行队列有数量限制, 当数量达到256个时会入队到全局运行队列. 本地运行队列的数据结构是环形队列, 由一个256长度的数组和两个序号(head, tail)组成.
全局运行队列的数据结构是链表, 由两个指针(head, tail)组成.
入队待运行的G后, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M
当M离开自旋状态并准备运行出队的G时, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M
当M离开自旋状态并准备休眠时, 会在离开自旋状态后再次检查所有运行队列, 如果有待运行的G则重新进入自旋状态
G从无缓冲的channel获取不到数据, G会保存状态并变为等待中(_Gwaiting)并添加到channel的队列
go的调用规范非常的简单, 所有参数都通过栈传递, 返回值也通过栈传递
参数和返回值都从低位到高位排列, go函数可以有多个返回值的原因也在于此. 因为返回值都通过栈传递了
TLS的全称是Thread-local storage, 代表每个线程的中的本地数据.
当函数发现栈空间不足时, 会申请一块新的栈空间并把原来的栈内容复制过去.
传递闭包给其他函数时会传递指向"闭包的内容"的指针
如果闭包修改了变量, 闭包中的参数会是指针而不是值, 修改时会修改到原来的位置上
m0是启动程序后的主线程, 这个m对应的实例会在全局变量m0中, 不需要在heap上分配,
m0负责执行初始化操作和启动第一个g, 在之后m0就和其他的m一样了.
g0是仅用于负责调度的G, g0不指向任何可执行的函数, 每个m都会有一个自己的g0,
在调度或系统调用时会使用g0的栈空间, 全局变量的g0是m0的g0.
g0会被设置到TLS中
第一个被调度的G会运行runtime.main
启动一个新的M执行sysmon函数, 这个函数会监控全局的状态并对运行时间过长的G进行抢占
G中保存调度数据是sched变量
M中的g0是用于调度的特殊g, 调度和执行系统调用时会切换到这个g
M获取g0就是从TLS中获取。
如果当前有空闲的P, 但是无自旋的M(nmspinning等于0), 并且主函数已执行则唤醒或新建一个M.
为了公平起见, 每61次调度从全局运行队列获取一次G, (一直从本地获取可能导致全局运行队列中的G不被运行)
sysmon中有netpool(获取fd事件), retake(抢占), forcegc(按时间强制执行gc), scavenge heap(释放自由列表中多余的项减少内存占用)等处理.
通过设置stackguard可以实现抢占,因为会触发栈扩张,栈扩张的时候检查是否等于一个特殊的常量,如果是,协程自身判断是否要抢占。
被枪占的g会到全局队列g中。
有疑问加站长微信联系(非本文作者)