面试题答案
一键面试Go语言调度器与Goroutine高效调度
- M:N调度模型基础:
- Go语言采用M:N调度模型,即多个用户级线程(Goroutine)映射到多个操作系统线程(M)上。这里的Goroutine是轻量级的用户态线程,而M是操作系统原生线程。
- 这种模型允许在一个M上并发执行多个Goroutine,同时也能将多个Goroutine分配到多个M上,从而充分利用多核CPU。
- 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操作结束),调度器会将其重新放入可运行队列,等待被调度执行。
- 与操作系统线程协作避免阻塞:
- 系统调用处理:当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有机会执行,进一步提升了调度的公平性和系统的响应性。
- 多核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的调度和多核资源的利用。