DEV Community

Alex Day
Alex Day

Posted on

Scheduling concurrency

Have been trying to look at 3 different languages: Go, Kotlin, Elixir/Erlang mostly to understand their concurrency models.

This article concerns mostly about Go's preemptive scheduling. Looking at the dense article on go: https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption.md

Here is what I understand.

Before Go 1.14: Cooperative preemption at function prologues

Go used compiler-inserted cooperative preemption points in function prologues up to and including Go 1.10. This means Go could only switch between concurrently-executing goroutines at specific points - and the compiler ensured that all local GC roots were known at those safe-points, enabling precise garbage collection.

The problems this caused were real and serious. In really extreme cases, it could cause a program to halt entirely.

For example, when a goroutine spinning on an atomic load starved out the goroutine responsible for setting that atomic.

They tried to fix this by inserting preemption checks at loop back-edges (the obvious next step), but even their most efficient approach - called "fault-based preemption" - added a geomean slowdown of 7.8% on a large benchmark suite.

It also had implementation downsides: it couldn't target specific threads or goroutines, was "sticky" in that they couldn't resume any loops until all loops were resumed, and interfered with debuggers.


Go 1.14+: Signal-based non-cooperative preemption

Non-cooperative preemption switches between concurrent execution contexts without explicit preemption checks or assistance from those contexts - the same way modern operating systems switch between threads. Without this, a single poorly-behaved goroutine can wedge a Go application, much like how a single poorly-behaved application could wedge an entire OS.

The mechanism: Go implements this by sending a POSIX signal to stop a running goroutine and capture its CPU state. If a goroutine is interrupted at a point that must be GC-atomic, the runtime simply resumes the goroutine and tries again later.

Why SIGURG specifically? It meets all the criteria: it's passed through by debuggers by default, isn't used internally by libc in mixed Go/C binaries, can happen spuriously without consequences, and is extremely unlikely to be used by an application for its real meaning - since out-of-band data is basically unused, and because SIGURG doesn't report which socket has the condition, making it pretty useless for its original purpose.


SIGUSR is a different beast altogether, it sits at the intersection of:

  • OS signals

  • Compiler internals

  • Runtime/GC Designs

It needs more reading. Will come back to that later.

Top comments (0)