为何Go语言被认为是部分抢占式的?
Go语言语境下的抢占式与协作式多任务澄清
我希望深入理解Go语言语境下的**抢占式(preemptive)和协作式(cooperative)**定义。维基百科中关于抢占式多任务的定义为:
在计算领域,抢占是指临时中断正在执行的任务,意图在稍后恢复执行。该中断由外部调度器(external scheduler)执行,无需任务的协助或配合。
维基提到的“external scheduler”,我猜测更具体来说是调度分派器,因为据我所知调度器仅负责选择下一个待执行的进程。
Go语言常被称为部分抢占式,因为其同步点/抢占点仅存在于函数调用处,而非任意指令位置。这一点我能理解,但根据维基的定义,抢占式需由外部调度器执行。
但既然CPU可以在执行过程中随时停止任意进程以切换到另一个进程,那所有进程或任务不都是抢占式的吗?希望能得到澄清!
补充说明
我能想到的唯一解释是我们讨论的是不同层级的抢占:一个针对进程,一个针对内核/用户线程。此时CPU调度器负责选择下一个进程,而Go调度器负责goroutine/线程。
核心澄清:抢占的层级差异是关键
你的猜测完全正确——我们讨论的是不同调度层级下的抢占行为,这也是理解Go“部分抢占式”的核心:
1. 内核级抢占(进程/内核线程层面)
CPU的硬件中断(比如时钟中断)触发的是内核调度器的抢占行为:
- 内核可以在任意指令点中断当前运行的进程/内核线程,无需目标任务配合,完全符合维基定义的“外部调度器”抢占。
- 这一层级的抢占是操作系统提供的基础能力,所有进程/内核线程都受其管控,所以从内核视角看,所有系统任务都是抢占式的。
2. 用户态调度器的抢占(Goroutine层面)
Go的Goroutine由用户态的Go调度器管理,它运行在用户空间,而非内核空间:
- 早期Go版本(1.14之前)是纯协作式调度:Goroutine必须主动在特定点(比如函数调用、channel操作、runtime调用)让出CPU,Go调度器才能切换到其他Goroutine。此时Goroutine的调度完全依赖任务自身配合,属于协作式。
- 1.14之后引入基于信号的异步抢占,但仍然是“部分抢占式”:
- Go调度器无法像内核那样在任意指令点中断Goroutine,只能在函数调用、栈检查点、或者收到内核信号(比如抢占信号)时触发切换。
- 这里的“外部调度器”是Go的用户态调度器,而非内核调度器。它的抢占能力受限于用户态的执行环境,无法突破到任意指令位置,所以称为“部分抢占式”。
3. 为什么Go不是完全抢占式?
用户态调度器没有内核的特权,无法直接干预CPU的指令执行:
- 内核可以通过硬件中断强制暂停CPU执行,但用户态程序(包括Go调度器)只能通过代码插入的检查点、或者借助内核信号来触发Goroutine切换。
- 比如,当一个Goroutine执行无限循环且没有函数调用时,在1.14之前会完全阻塞M(内核线程),1.14之后虽然可以通过信号抢占,但仍然需要Goroutine执行到某个安全点才能完成切换,并非真正的“任意指令点”抢占。
4. 关于“external scheduler”的定义修正
维基的“external scheduler”指的是独立于被调度任务的调度器,而非单纯的“调度分派器”:
- 内核调度器对于进程来说是外部的;Go调度器对于Goroutine来说也是外部的。
- 协作式调度中,调度器的切换依赖任务主动触发,此时调度器的控制权在任务手中;而抢占式调度中,调度器可以主动中断任务,控制权在调度器手中。
内容的提问来源于stack exchange,提问作者HelloWorld
相关产品推荐
相关产品推荐

