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.
SIGURG 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.
답글 남기기