skip to content

Does a goroutine running a tight CPU-bound loop need runtime.Gosched() to let other goroutines run?

level: juniorimportance: should knowfreq 40%

answer

  1. who decides when a goroutine stops
  2. the runtime does not wait to be asked
  3. a call-free loop is still interruptible
  4. Go 1.14 changed this answer
  5. Gosched only yields, it does not throttle

basics

~20 s

No. Since Go 1.14 the runtime interrupts a long-running goroutine on its own using a signal, so a tight loop no longer starves the rest of the program. runtime.Gosched() yields voluntarily and is rarely needed.

solid answer

~50 s

`runtime.Gosched()` yields the processor: the calling goroutine goes back on a run queue and something else runs. You almost never need it. Go's goroutine scheduler preempts on its own — cooperatively at function calls, and since Go 1.14 asynchronously as well: a runtime monitor notices a goroutine that has held its processor for about 10 ms and has a signal delivered to the OS thread, which interrupts the loop and lets another goroutine in. That was not always true. In Go 1.13 and earlier a loop body containing no function call had no preemption point at all, so a single `for {}` could stall a garbage collection, and with GOMAXPROCS set to 1 it could wedge the whole program. Either way the loop still burns a full CPU — preemption shares time, it does not reduce work.

code

go · 9 lines
go
// checksum hashes a large buffer in place, calling nothing per byte.
func checksum(buf []byte) uint64 {
	h := uint64(14695981039346656037)
	for _, b := range buf {
		h ^= uint64(b)
		h *= 1099511628211
	}
	return h
}

go deeper

for a junior

Be ready to say that Go's runtime preempts long-running goroutines for you and that runtime.Gosched() is a manual yield you rarely write. Know that a busy loop still uses a whole CPU.

for a middle

Explain both ways a goroutine is stopped - the stack check at a function call, and the signal the runtime sends after a goroutine has run about 10 ms - and why Go 1.14 was the turning point.

for a senior

Show what preemption latency costs a latency-sensitive service: CPU-bound goroutines sharing a process still add delay before other work is scheduled. Be able to say whether a pause you measured is scheduling delay or something else.

for a principal

Own the placement decision: whether heavy compute belongs in the same process as request handling at all, and what GOMAXPROCS and CPU limits you give each workload when they must share a container.

## What the question is really asking `runtime.Gosched()` is a standard-library call that tells Go's runtime: stop running me for now, put me back on a run queue, and run something else. Programmers coming from cooperative-threading systems assume Go needs such a call sprinkled through CPU-heavy code so that other goroutines get a turn. In modern Go it does not. ## How a goroutine gets stopped Goroutines are not OS threads. Many goroutines are multiplexed onto a small number of OS threads, and it is Go's own runtime — not the operating system — that decides which goroutine occupies a scheduling slot. So the runtime needs a way to take a running goroutine off the CPU. It has two. **Cooperative preemption at safe points.** Almost every Go function begins with a check that there is enough stack left to run it. The runtime can poison that limit so the check fails, which sends control into the runtime instead of growing the stack. Every ordinary function call therefore doubles as a place the runtime can stop you — a *safe point*. This costs nothing when preemption is not requested, which is why Go uses it. **Asynchronous preemption.** Some loops call nothing at all: a checksum over a byte buffer, a compression inner loop, a busy-wait. Such a loop reaches no safe point, ever. Since Go 1.14 the runtime handles that by having the OS deliver a signal to the thread running the goroutine; the signal handler interrupts the loop mid-flight and diverts the thread into the scheduler. This is why the answer to the question is now *no*. ## What life was like before Go 1.14 In Go 1.13 and earlier the cooperative path was the only path. A goroutine spinning in a call-free loop could not be stopped, and the consequences were sharp: - A garbage collection needs every goroutine to pause briefly. One unpreemptible loop meant the pause request waited for the loop to finish — which, for an infinite loop, is never. - With `GOMAXPROCS=1`, a program that started such a goroutine and then tried to do anything requiring the world to stop would simply hang, with no deadlock message and no panic. The workaround at the time really was to insert a `runtime.Gosched()` or some other call into the loop. That advice is stale; treat it as a historical artefact when you meet it in an old blog post. ## What preemption does and does not buy you Preemption is about *fairness of scheduling*, not about *reducing work*: - A goroutine hashing a large buffer still consumes one CPU for as long as the hashing takes. Preemption interleaves it with other goroutines; it does not slow it down or throttle it. - The number of goroutines running Go code simultaneously is bounded by `GOMAXPROCS`. If you start eight CPU-bound goroutines on a four-processor setting, four run at a time and the runtime rotates them. - Preemption is not instantaneous. The runtime acts after a goroutine has been running roughly 10 ms, and the actual stop can come later. If you have latency-sensitive work sharing a process with heavy compute, that delay is visible in your tail latencies. The fix is architectural — separate the workloads, or bound how much CPU the compute gets — not a `runtime.Gosched()` call. ## When would you actually call runtime.Gosched()? Rarely, and mostly in low-level code: a hand-rolled spin loop waiting on an atomic flag, or a test that wants to give another goroutine an opportunity to make progress. It is **not** a synchronisation primitive — it guarantees no ordering and no wakeup. If you find yourself reaching for it to make a program correct, a channel, a `sync.Mutex` or a `sync.WaitGroup` was the right tool. A related call worth keeping straight: `runtime.Goexit()` does not yield, it *terminates* the calling goroutine after running its deferred calls. ## What a good answer sounds like No, you do not need it; Go's runtime preempts a long-running goroutine by itself, at function calls and — since Go 1.14 — asynchronously by signalling the thread, so a call-free loop is preemptible too. `runtime.Gosched()` is a manual yield you almost never write, and it does not reduce the CPU the loop consumes.

  • What is the difference between runtime.Gosched() and runtime.Goexit()?
    `runtime.Gosched()` yields the processor and the goroutine resumes later from a run queue. `runtime.Goexit()` terminates the calling goroutine, running its deferred calls first, and never returns. Calling `Goexit` from the main goroutine does not return from `main`, so the program keeps running other goroutines and crashes once none are left.
  • Does preemption reduce the CPU a busy loop consumes?
    No. Preemption interleaves goroutines; the loop still needs the same number of cycles and still occupies a processor while it runs. To bound its CPU you change the program — fewer worker goroutines, a smaller `GOMAXPROCS`, or moving the work out of the latency-sensitive process. Preemption only guarantees that other goroutines get turns.
  • When is runtime.Gosched() genuinely useful?
    Almost never in application code. It shows up in hand-rolled spin loops waiting on an atomic flag, and occasionally in test code that wants to give another goroutine an opportunity to run. It is not a synchronisation primitive: it makes no ordering or wakeup guarantee, so needing it usually means a channel or mutex was the right answer.

Cooperative preemption is a driver who lets you merge only when he happens to glance at the mirror. Asynchronous preemption is a traffic light that turns red whether or not he was looking.

saying these in an interview costs you the question

  • Says goroutines only switch when they block or call a function
  • Claims runtime.Gosched() is required in normal application code
  • Thinks preemption lowers the CPU a busy loop consumes
  • Confuses runtime.Gosched with runtime.Goexit
  • Believes GOMAXPROCS=1 means only one goroutine ever makes progress