本文是《循序渐进Go语言》的第六篇-Go并发调度。本文是学习《Go语言学习笔记》的并发调度一章,然后结合阅读源码的总结,希望对你有所帮助~
1 概述
Go语言在操作系统的进程、线程基础上又进一步做了更高层次的抽象。通过三种基本对象相互协作,来实现在用户空间管理和调度并发任务。
三种对象分别是:(为了概念准确,直接引用《Go语言学习笔记》的原文)
- Processor (简称P),用来控制可同时并发执行的任务数量。每个工作线程都必须绑定一个有效P才被允许执行。P还为线程提供执行资源,比如内存分配、本地任务队列等。线程独享所绑定的P资源,可在无锁状态下执行高效操作
- 进程内的一切都已goroutine(简称G) 方式运行,包括运行时相关服务,以及main.main入口函数。G并非执行体,它仅仅**保存并发任务状态,为任务执行提供所需的栈内存空间。G任务创建后,要放入P队列或者全局队列,等待工作线程调度执行。
- 工作线程(简称M)。和P绑定,以调度循环方式不停地执行G并发任务。M通过修改寄存器,将执行栈指向G自带的栈内存,并在此空间内分配堆栈帧,执行任务函数。当需要中途切换时,只需要将相关寄存器值保存回G空间即可维持状态。任何M都可以据此恢复执行。线程仅仅负责执行,不再持有状态。这是并发任务跨线程调度实现多路复用的根本所在
上面文字看明白了吗?我第一遍看的时候,也没啥概念。等到看完全章再回头来整理,发现句句宝贵。
为了更明白一些,我来画个图说明一下三者的关系:
G-P-M.png
2 调度器初始化 (schedInit)
主要经历如下步骤:
1) 设置M的最大数量
2) 初始化当前的M
3) 设置P的数量,一般为cpu核数
4)调整P的数量(这个过程中会新建一批P,放入空闲链表;另外调整过程也会发生在startTheWorld阶段,如果P之前太多,则会回收一部分)
调度器初始化之后,才会执行main 函数,main函数也是一个用户应用程序,也是启动一个goroutine。
3 任务
每次我们执行
go func (){}
其实编译之后,就会发现这是调用了系统的runtime.newproc操作。
那么这个函数主要执行了什么呢?
1) 获取队列中的空闲G。P队列中的G 是会复用的,如果队列中没有空闲P,则调用malg 创建新的G。
2) 为空闲的G设置栈空间
3) 保留现场
4) 设置调用者的PC(方便任务执行完毕之后,回到调用处)以及任务函数。
5)放入P队列,等待唤醒线程M执行。P队列按照优先级分了3级。先放本地,如果本地满了,则一次性将一半的任务迁移到全局队列。其目的是方便让其他空闲的P执行。
讲到这儿,我们应该看出,G一般会经过
“新建” -->"初始化前"-->"初始化"-->"调度执行"-->"执行完毕" 这几个状态。
如果任务执行完毕,G是可以继续放入P队列,作为空闲G,迎接新的任务的到来。
4 线程M
上一节我们提到,新任务创建之后,放入P队列,然后去唤醒线程M执行。
那么线程M从哪儿来呢,在存储在sched结构中。
M的获取也有两种,一种是获取空闲的M,另外一种是新建M(每个新建的M都会自带一个分配了8m空间的G0。G0主要是用来执行非用户任务,运行时的管理指令)。
被唤醒的M会陷入执行循环,从各个场所获取任务,并执行。有些时候甚至偷取其他P的任务。只有M被剥夺了P (比如因为执行时间太长,被抢占了)才会进入休眠状态。
5 线程执行
线程启动,会执行mstart这个函数。启动时必须绑定一个有效的P,P为M提供cache,以便为工作线程提供对象内存分配。每一个P都有一个mcache的结构。我们看下mcache的部分解释
Per-thread (in Go, Per-P) cache for small objects。
我们之前讲内存分配时,有提到说每个线程独占一个cache, 当时的cache就是这儿的cache。
mcache 中还有一个span的数组结构 “alloc [67]*mspan” 。是不是越看关系越大了,哈哈。
其执行流程:
1) 获取任务:要么从P本地获取,要么从其他地方获取(比如全局队列,网络队列,偷其他P的任务)
2)执行 主要是execute函数。这个函数中主要是通过汇编gogo函数来执行。
3)执行完毕,清理现场,然后跳回到调用处。
6 连续栈
其实栈的分配、回收、释放跟内存策略都一样。就不展开讲了。
G中有个stackguard0的指针,可以通过它跟SP比较,用于判断是否需要扩充栈了。如果扩充,就找一个原来两倍大小的栈,然后将数据copy过去,然后替代旧栈即可。
7 系统调用与监控
7.1 系统调用
为了支持并发,Go对syscall 进行了包装,以便在长时间阻塞时能切换执行其他任务。
监控线程sysmon负责将因系统调用而长时间阻塞的P抢回,将P用于执行其他任务。
7.2 监控
监控线程sysmon 保障了内存分配、垃圾回收、并发调度的有效执行。
除了上文提到的抢占调度,sysmon还提供如下的功能:
释放闲置超过5分钟的span物理内存
如果超过2分钟没有垃圾回收,则强制执行
将长时间未处理的netpoll结果添加到任务队列
其实sysmon 就是个for循环,会定期对上述的几个功能进行检查,如果到了特定时间,就执行预定的方法。
如果遇到GC,sysmon 会进入休眠状态。
7.3 抢占调度
所谓的抢占调度,只是在目标G上设置一个抢占标志,当该任务调用某个函数时,会检查这个标志,从而决定暂停这个任务。
8 stopTheWorld
stopTheWorld 会循环等待目标任务进入一个安全点后主动暂停。其核心流程就是向所有正在运行的G发送抢占调度,然后等。。。如果还没停,就继续发抢占调度。
9 总结
本文讲解了Go并发调度的机制。从G-P-M的关系入手,然后讲解了系统初始化时,调度器做的一系列操作。接着从一个任务创建、启动,到执行 讲解了G、P、M 都做了什么事情。 最后讲了系统调用、监控以及stopTheWorld这种暂停的机制。
10 参考文献
《Go学习笔记》
Go源码 1.8 版本。
11 其他
本文是《循序渐进go语言》的第六篇-《Go并发调度》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~
网友评论