ℹ️ This article is based on Go 1.14.
The preemption is an important part of the scheduler that lets it distribute the running time among the goroutines. Indeed, without preemption, a long-running goroutine that hogs the CPU would block the other goroutines from being scheduled. The version 1.14 introduces a new technique of asynchronous preemption, giving more power and control to the scheduler.
For more details about the previous behavior and its drawback, I suggest you read my article “Go: Goroutine and Preemption.”
WorkflowLet’s start with an example where preemption is needed. Here is a code where many goroutines loop for a while without any function call, meaning no opportunity for the scheduler to preempt them:
However, when visualizing the traces from this program, we clearly see the goroutines are preempted and switch among them:
We can also see that all the blocks representing the goroutines have the same length. The goroutines get almost the same running time (around 10/20ms):
The asynchronous preemption is triggered based on a time condition. When a goroutine is running for more than 10ms, Go will try to preempt it.
sysmon
G7gsignal
gsignal
Implementation
SIGURG
- It should be a signal that’s passed-through by debuggers by default.
- It shouldn’t be used internally by libc in mixed Go/C binaries […].
- It should be a signal that can happen spuriously without consequences.
- We need to deal with platforms without real-time signals […].
Then, once the signal is injected and received, Go needs a way to stop the current goroutine when the program resumes. To achieve this, Go will push an instruction in the program counter, to make it looks like the running program called a function in the runtime. This function parks the goroutine and hands it to the scheduler that will run another one.
We should note that Go cannot stop the program anywhere; the current instruction must be a safe point. For instance, if the program is currently calling the runtime, it would not be safe to preempt the goroutine since many functions in the runtime should not be preempted.
This new preemption also benefits the garbage collector that can stop all the goroutines in a more efficient way. Indeed, stopping the world is now much easier, Go just has to send a signal to every running thread. Here is an example when the garbage collector is running:
Then, each thread receives the signal and pauses the execution until the garbage collector starts the world again.
For more information about the phase “Stop the World,” I suggest you read my article “Go: How Does Go Stop the World?”
GODEBUG=asyncpreemptoff=1
ℹ️ 本文基于 Go 1.14。
抢占是调度器的重要部分,基于抢占调度器可以在各个协程中分配运行的时间。实际上,如果没有抢占机制,一个长时间占用 CPU 的协程会阻塞其他的协程被调度。1.14 版本引入了一项新的异步抢占的技术,赋予了调度器更大的能力和控制力。
我推荐你阅读我的文章”Go:协程和抢占“[1]来了解更多之前的特性和它的弊端。
工作流
我们以一个需要抢占的例子来开始。下面一段代码开启了几个协程,在几个循环中没有其他的函数调用,意味着调度器没有机会抢占它们:
然而,当把这个程序的追踪过程可视化后,我们清晰地看到了协程间的抢占和切换:
我们还可以看到表示协程的每个块儿的长度都相等。所有的协程运行时间相同(约 10 到 20 毫秒)。
异步抢占是基于一个时间条件触发的。当一个协程运行超过 10ms 时,Go 会尝试抢占它。
sysmonsysmon
G7gsignal
gsignal
实现
SIGURG
- 它应该是调试者默认传递过来的一个信号。
- 它不应该是 Go/C 混合二进制中 libc 内部使用的信号。
- 它应该是一个可以伪造而没有其他后果的信号。
- 我们需要在没有实时信号时与平台打交道。
然后,当信号被注入和接收时,Go 需要一种在程序恢复时能终止当前协程的方式。为了实现这个过程,Go 会把一条指令推进程序计数器,这样看起来运行中的程序调用了运行时的函数。该函数暂停了协程并把它交给了调度器,调度器之后还会运行其他的协程。
我们应该注意到 Go 不能做到在任何地方终止程序;当前的指令必须是一个安全点。例如,如果程序现在正在调用运行时,那么抢占协程并不安全,因为运行时很多函数不应该被抢占。
这个新的抢占机制也让垃圾回收器受益,可以用更高效的方式终止所有的协程。诚然,STW 现在非常容易,Go 仅需要向所有运行的线程发出一个信号就可以了。下面是垃圾回收器运行时的一个例子:
然后,所有的线程都接收到这个信号,在垃圾回收器重新开启全局之前会暂停执行。
如果你想了解更多关于 STW 的信息,我建议你阅读我的文章”Go:Go 怎样实现 STW?“[4]。
GODEBUG=asyncpreemptoff=1
via: https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c
https://mp.weixin.qq.com/s/Wp15aOLeYhZYla275TzISw