MST

星途 面试题库

面试题:Go函数并发调用的底层调度实现

深入讲解Go函数在并发调用场景下,其底层的调度器(Goroutine调度)是如何工作的,包括M:N模型的具体实现、调度队列的管理以及函数执行过程中上下文切换的细节。
48.8万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

1. Go函数在并发调用场景下调度器概述

Go语言的调度器采用了M:N模型,即多个Goroutine(用户态线程)映射到多个操作系统线程(内核态线程)上。这使得Go能够高效地管理并发任务,在一个操作系统线程上并发执行多个Goroutine,避免了创建大量操作系统线程带来的资源开销。

2. M:N模型的具体实现

  • Goroutine(G):是Go语言的轻量级线程,由Go运行时(runtime)管理。每个Goroutine都有自己的栈空间和执行上下文,其创建和销毁的开销远小于操作系统线程。
  • 操作系统线程(M):是内核态线程,由操作系统管理。M负责执行Goroutine。Go运行时会在需要时创建、销毁和复用M。
  • 调度器(P):调度器是Go运行时的核心组件,它负责管理Goroutine的调度队列,并将Goroutine分配给M执行。P的数量通常与CPU核心数相关,默认情况下P的数量等于CPU核心数,通过runtime.GOMAXPROCS函数可以调整。

3. 调度队列的管理

  • 全局队列:Go调度器维护了一个全局的Goroutine队列,当新创建的Goroutine数量过多,超过了每个P本地队列的容量时,多余的Goroutine会被放入全局队列。全局队列由一个互斥锁保护,以确保并发安全。
  • 本地队列:每个P都有一个本地的Goroutine队列,M优先从P的本地队列中获取Goroutine来执行。这样可以减少锁的竞争,提高调度效率。当本地队列空了时,M会尝试从全局队列中获取一批Goroutine,或者从其他P的本地队列中偷取一半的Goroutine(工作窃取算法)。

4. 函数执行过程中上下文切换的细节

  • 主动切换:当Goroutine执行到runtime.Gosched()函数时,会主动放弃CPU使用权,将自己放入当前P的本地队列尾部,然后调度器会从队列中选择另一个Goroutine执行。另外,当Goroutine进行系统调用(如I/O操作)时,会将自己从当前M上分离,M继续执行其他Goroutine,待系统调用完成后,该Goroutine会被重新放入队列等待调度。
  • 被动切换:Go调度器使用协作式调度,没有时间片的概念。但是当一个Goroutine执行时间过长时,调度器会通过插入抢占点的方式进行被动调度。在Go 1.14及以后版本,引入了基于信号的抢占机制,当一个Goroutine运行超过一定时间(默认为10ms),操作系统会向对应的M发送一个信号,M在接收到信号后会在合适的时机(如函数调用、系统调用等)检查抢占标志,然后将当前Goroutine挂起,调度其他Goroutine执行。上下文切换时,调度器需要保存当前Goroutine的寄存器状态(如程序计数器、栈指针等),并恢复下一个要执行的Goroutine的寄存器状态,这样就能保证Goroutine的执行连续性。