面试题答案
一键面试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的执行连续性。