ℹ️ 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.”
Let’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.
The preemption is initiated by the thread
sysmon, dedicated to watching after the runtime, long-running goroutines included. Once a goroutine is detected running more than 10ms, a signal is emitted to the current thread for its preemption:
Then, once the message is received by the signal handler, the thread is interrupted to handle it, and therefore does not run the current goroutine anymore —
G7 in our example. Instead,
gsignal is scheduled to manage the incoming signal. Since it finds out it is a preemption instruction, it sets instruction up to stop the current goroutine when the program resumes after the signal handling. Here is a diagram of this second phase:
For more details about
gsignal, I suggest you read my article “Go: gsignal, Master of Signals.”
The first detail of the implementation we have seen is the chosen signal
SIGURG. This choice is well explained in the proposal “Proposal: Non-cooperative goroutine preemption”:
- 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?”
At last, this feature is shipped with a flag to deactivate the asynchronous preemption. You can run your program with
GODEBUG=asyncpreemptoff=1, that will allow you to debug your program if you see anything incorrect due to the upgrade to Go 1.14, or see how your application performs with or without asynchronous preemption.