Go: How Does Go Stop the World?

ℹ️ This article is based on Go 1.13.
A “Stop the World” (STW) is a crucial phase in some garbage collector algorithms to get track of the memory. It suspends the execution of the program to scan the memory roots and add write barriers. Let’s review how it works internally and the potential issues it could face.
Stop the World
Stopping the program means stopping the running goroutines. Here is a simple program that will “Stop the World”:
func main() {
runtime.GC()
}Running the garbage collector will trigger two “Stop the World” phases.
For more information about the garbage collector cycle, I suggest you read my article “Go: How Does the Garbage Collector Mark the Memory?”
The first step of this phase is to preempt all running goroutines:

Once the goroutines are preempted, they will be stopped at a safe point. Meanwhile, the processors P — running code or in the idle list — will be marked as stopped to not be used to run any code:

Then, the Go scheduler will run and detach each M from their respective P and put them in the idle lists:

Regarding the goroutines running on each M, they will wait in the global queue:

Then, once the world is stopped, the only active goroutine can safely run and start the world when the work is done. The tracing will help to see when this phase happens:

System calls
The phase “Stop the World” could impact system calls as well since they could return while the world is stopped. Let’s take an example with a program that intensively does system calls and see how it is handled:
func main() {
var wg sync.WaitGroup
wg.Add(10) for i := 0; i < 10; i++ {
go func() {
http.Get(`https://httpstat.us/200`)
wg.Done()
}()
} wg.Wait()
}Here is the tracing:

The system call here is exiting while the world is stopped. However, since there is no available P — they are all marked as stopped, as seen in the previous section — the goroutine will be put in the global queue and will run later when the world resumes.
Latencies
The third step of the “Stop the World” involves the M’s to be all detached from their P. However, Go will wait for them to stop voluntarily: when the scheduler runs, during syscall, etc. Waiting for a goroutine to be preempted should be fast, but in some cases, it could lead to some latencies. Let’s take an example that will show an extreme case:
func main() {
var wg sync.WaitGroup var t int
for i := 0;i < 20 ;i++ {
wg.Add(1)
go func() {
for i := 0;i < 1000000000 ;i++ {
t++
}
wg.Done()
}()
}
wg.Wait()
runtime.GC()
}Here, the “Stop the World” phase takes 2.6 seconds:

A goroutine without function calls will not be preempted, and its P will not be released before the end of the task. That will force the “Stop the World” to wait for it.
ℹ️ The issue raised in this section is now fixed in Go 1.14 with the asynchronous preemption. Indeed, Go is now able to send signals to each P to preempt the running goroutine. For more details about asynchronous preemption, I suggest you read “Go: Asynchronous Preemption.”






