MST

星途 面试题库

面试题:Go调度器如何实现Goroutine高效调度并与线程协作

深入描述Go语言调度器(如M:N调度模型)是如何实现Goroutine的高效调度,以及它怎样与操作系统线程进行协作,以避免阻塞并充分利用多核CPU资源。
19.7万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

Go语言调度器与Goroutine高效调度

  1. M:N调度模型基础
    • Go语言采用M:N调度模型,即多个用户级线程(Goroutine)映射到多个操作系统线程(M)上。这里的Goroutine是轻量级的用户态线程,而M是操作系统原生线程。
    • 这种模型允许在一个M上并发执行多个Goroutine,同时也能将多个Goroutine分配到多个M上,从而充分利用多核CPU。
  2. Goroutine的调度实现
    • Goroutine队列:Go调度器维护了多个Goroutine队列,包括全局队列和每个M对应的本地队列。新创建的Goroutine通常会被放入全局队列,而正在运行的M会优先从自己的本地队列中获取Goroutine执行。如果本地队列为空,M会尝试从全局队列或者其他M的本地队列中窃取Goroutine(工作窃取算法),以保证所有M都能充分工作。
    • 调度器组件:Go调度器主要由三个组件组成:G(Goroutine)、M(操作系统线程)和P(Processor)。P是调度的上下文,它包含了运行Goroutine所需要的资源,如本地Goroutine队列等。每个M必须绑定一个P才能运行Goroutine。P的数量通常在程序启动时根据CPU核心数确定,默认值是runtime.GOMAXPROCS的值,这就使得Goroutine能被分配到不同的CPU核心上执行。
    • 协作式调度:Goroutine之间采用协作式调度,当一个Goroutine执行系统调用(如I/O操作)或者调用runtime.Gosched()等让出CPU的函数时,它会主动暂停执行,调度器会将其从运行状态切换到等待状态,并将M释放,以便M去执行其他可运行的Goroutine。当Goroutine等待的事件完成(如I/O操作结束),调度器会将其重新放入可运行队列,等待被调度执行。
  3. 与操作系统线程协作避免阻塞
    • 系统调用处理:当Goroutine执行系统调用时,Go调度器会将其与M分离,M进入阻塞状态等待系统调用完成。与此同时,调度器会将P与其他空闲的M绑定,继续执行其他Goroutine,从而避免整个系统因一个Goroutine的阻塞而停止运行。例如,当一个Goroutine执行网络I/O操作时,调度器会释放绑定的M,让其他Goroutine在该P上运行。当I/O操作完成后,对应的Goroutine会被重新调度到一个M上继续执行。
    • 抢占式调度:Go 1.14版本引入了基于信号的抢占式调度机制,用于处理长时间运行的Goroutine,防止其长时间占用M而导致其他Goroutine无法执行。当一个Goroutine运行一段时间后,调度器会发送一个信号给对应的M,强制该Goroutine暂停执行,然后调度器将该Goroutine放入可运行队列,让其他Goroutine有机会执行,进一步提升了调度的公平性和系统的响应性。
  4. 多核CPU资源利用
    • 多P并行:由于P的数量与CPU核心数相关,多个P可以并行运行在不同的CPU核心上,每个P上可以运行多个Goroutine。这就使得多个Goroutine可以真正并行执行在多核CPU上,充分利用多核资源。例如,一个具有4个CPU核心的系统,默认会创建4个P,每个P可以绑定一个M并执行Goroutine,从而实现多个Goroutine在不同核心上的并行执行。
    • 动态调整:Go调度器会根据系统负载动态调整Goroutine的调度策略,如在高负载情况下,通过工作窃取算法更频繁地在M之间平衡Goroutine的负载,确保多核CPU资源得到充分利用。同时,调度器也会根据系统资源的变化,如CPU核心数的动态调整(如在虚拟化环境中),自动调整P的数量,以优化Goroutine的调度和多核资源的利用。