Go: Timers’ Life Cycle
By: Vincent Blanchon
Tags: Go, Internals, Golang

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Go 1.14.

Timers are useful in executing code at some point in the future. Go internally manages the created timers along with planning their execution. The latter one can be a bit tricky since the Go scheduler is a cooperative scheduler, meaning a goroutine has to stop by itself (blocking on a channel, system calls, etc.) or paused by the scheduler at some scheduling point.

For more information about the preemption, I suggest you read my article “Go: Goroutine and Preemption.”

Cycle of Life

Here is a basic example of a timer:

When a timer is created, it is saved on an internal list of timers linked to the current P. Here is a representation with the previous code:

For more information about the G, M, P model, I suggest you read my article “Go: Goroutine, OS Thread and CPU Management.”

As you can see in the diagram, once the timer is created it registers an internal callback that will call the user callback with the keyword go , converting it to a goroutine.

Then, the timers will be managed from the scheduler. At each round of scheduling, it checks if the timers are ready to run, and if so, prepare them to run. Indeed, since the Go scheduler does not run any code itself, running the timer’s callback will enqueue its goroutine to the local queue. Then, the goroutine will run when the scheduler picks it up in the queue. Here is the diagram:

Depending on the size of the local queue, the timer could run with some minor delay. Indeed, thanks to the asynchronous preemption in Go 1.14, the goroutines are preempted after 10ms of running time, reducing the probability of delay.

Delay

To understand the possibility of delay, let’s analyze a case when a large number of timers are created from the same goroutine. Since timers are linked to the current P , a busy P would not able to run its timers. Here is a program that creates hundreds of timers and remains busy for the rest of the time:

Here is the tracing where we can see the goroutine hogging the processor:

The block representing the running goroutine is divided into a large number of smaller block, due to the asynchronous preemption.

For more information about the asynchronous preemption, I suggest you read my article “Go: Asynchronous Preemption.”

Among the blocks, one space looks bigger than the other ones. Let’s zoom in on it:

This space happens when the timers have to run. At this time, the current goroutine has been preempted and replaced by the Go scheduler. The scheduler converts the timers into runnable goroutines, as we can see highlighted in the picture.

However, the Go scheduler of the current thread is not the only one running the timer. Go implements a timer-stealing strategy to ensure timers could be run by another P when the current one is quite busy. Thanks to the asynchronous preemption, it is less likely to happen, but in our example and due to the number of timers, it happened. Here is a diagram:

If we do not take into account the timer-stealing, here is what will happen:

All the goroutines holding the timers are added to the local queue. Then, it will be dispatched thanks to work-stealing between P .

For more information about the work-stealing in Go, I suggest you read my article “Go: Work-Stealing in Go Scheduler.”

Thanks to the asynchronous preemption and the work-stealing, delays are very unlikely to happen.