✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06
📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
151. Go 调度器调度对象
Go 调度器是属于 Go runtime 中的一部分,Go runtime 负责实现 Go 的并发调度、垃圾回收、内存堆栈管理等关键功能。
152. Go 调度器被调度对象
G 的来源
- P 的 runnext(只有 1 个 G,局部性原理,永远会被最先调度执行)
- P 的本地队列(数组,最多 256 个 G)
- 全局 G 队列(链表,无限制)
- 网络轮询器 network poller(存放网络调用被阻塞的 G)
P 的来源
- 全局 P 队列(数组,GOMAXPROCS 个 P)
M 的来源
- 休眠线程队列(未绑定 P,长时间休眠会等待 GC 回收销毁)
- 运行线程(绑定 P,指向 P 中的 G)
- 自旋线程(绑定 P,指向 M 的 G0)
其中运行线程数 + 自旋线程数 <= P 的数量(GOMAXPROCS),M 个数 >= P 个数
153. Go 调度器调度流程
协程的调度采用了生产者 - 消费者模型,实现了用户任务与调度器的解耦。
生产端我们开启的每个协程都是一个计算任务,这些任务会被提交给 go 的 runtime。如果计算任务非常多,有成千上万个,那么这些任务是不可能同时被立刻执行的,所以这个计算任务一定会被先暂存起来,一般的做法是放到内存的队列中等待被执行。
G 的生命周期:G 从创建、保存、被获取、调度和执行、阻塞、销毁,步骤如下:
步骤 1:创建 G,关键字 go func() 创建 G。
步骤 2:保存 G,创建的 G 优先保存到本地队列 P,如果 P 满了,则会平衡部分 P 到全局队列中。
步骤3:唤醒或者新建 M 执行任务,进入调度循环(步骤 4, 5, 6)。
步骤 4:M 获取 G,M 首先从 P 的本地队列获取 G,如果 P 为空,则从全局队列获取 G,如果全局队列也为空,则从另一个本地队列偷取一半数量的 G(负载均衡),这种从其它 P 偷的方式称之为 work stealing。
步骤 5:M 调度和执行 G,M 调用 G.func() 函数执行 G。
- 如果 M 在执行 G 的过程发生系统调用阻塞(同步),会阻塞 G 和 M(操作系统限制),此时 P 会和当前 M 解绑,并寻找新的 M,如果没有空闲的 M 就会新建一个 M ,接管正在阻塞 G 所属的 P,接着继续执行 P 中其余的 G,这种阻塞后释放 P 的方式称之为 hand off。当系统调用结束后,这个 G 会尝试获取一个空闲的 P 执行,优先获取之前绑定的 P,并放入到这个 P 的本地队列,如果获取不到 P,那么这个线程 M 变成休眠状态,加入到空闲线程中,然后这个 G 会被放入到全局队列中。
- 如果 M 在执行 G 的过程发生网络 IO 等操作阻塞时(异步),阻塞 G,不会阻塞 M。M 会寻找 P 中其它可执行的 G 继续执行,G 会被网络轮询器 network poller 接手,当阻塞的 G 恢复后,G1 从 network poller 被移回到 P 的 LRQ 中,重新进入可执行状态。异步情况下,通过调度,Go scheduler 成功地将 I/O 的任务转变成了 CPU 任务,或者说将内核级别的线程切换转变成了用户级别的 goroutine 切换,大大提高了效率。
步骤 6:M 执行完 G 后清理现场,重新进入调度循环(将 M 上运⾏的 goroutine 切换为 G0,G0 负责调度时协程的切换)
其中步骤 2 中保存 G 的详细流程如下:
- 执行 go func 的时候,主线程 M0 会调用 newproc() 生成一个 G 结构体,这里会先选定当前 M0 上的 P 结构。
- 每个协程 G 都会被尝试先放到 P 中的 runnext,若 runnext 为空则放到 runnext 中,生产结束。
- 若 runnext 满,则将原来 runnext 中的 G 踢到本地队列中,将当前 G 放到 runnext 中,生产结束。
- 若本地队列也满了,则将本地队列中的 G 拿出一半,放到全局队列中,生产结束。