skip to content

In Go, does one goroutine blocked in a file-read syscall stop the other goroutines from running?

level: juniorimportance: must knowfreq 62%

answer

  1. the thread is stuck, not the work
  2. scheduling contexts can move between threads
  3. the P detaches from the M
  4. another thread drains that P's run queue
  5. sysmon hands the P off

basics

~20 s

No. When a goroutine enters a system call that blocks, the Go runtime detaches its P (the scheduling context) from that OS thread, and another thread picks the P up and keeps running the remaining goroutines.

solid answer

~50 s

No — only that one goroutine stops. Go runs goroutines on OS threads through a fixed number of scheduling contexts called Ps, and `GOMAXPROCS` sets how many exist. When a goroutine issues a system call, the runtime hands off its P: for a call it knows will block it releases the P immediately, and otherwise the runtime's monitor thread `sysmon` takes the P away if the call is still outstanding when it next looks. Another OS thread — woken from the idle list or newly created — picks that P up and keeps draining its run queue. The blocked thread stays stuck in the kernel until the call returns; then the goroutine tries to grab a P back, and if none is free it goes on the global run queue while its thread parks for reuse. The visible cost is threads, not stalled goroutines.

go deeper

for a junior

Be ready to state plainly that the other goroutines keep running, and that the goroutine stuck in the kernel costs one OS thread while it waits.

for a middle

You are expected to describe the hand-off: the scheduling context is detached from the thread stuck in the kernel, another thread picks it up, and the returning goroutine reacquires a context or goes to the global run queue.

for a senior

Connect it to what you see in production — thread counts well above GOMAXPROCS, and blocking work that needs a concurrency cap rather than a scheduler tweak.

for a principal

Own it as a capacity question: which calls in your services block a thread, what that costs per instance across a fleet, and whether the answer is a limit, a different API, or accepting the threads.

**Short answer: no.** A goroutine sitting in a blocking system call — a `read` on a regular file, a name lookup, a `stat` — stops that one goroutine and occupies one OS thread. Everything else in the program keeps running. ## The three moving parts Go multiplexes goroutines onto OS threads through three runtime objects: **G** is a goroutine, **M** is an OS thread, and **P** is a scheduling context that owns a run queue of runnable goroutines. A thread may only execute Go code while it holds a P, and `GOMAXPROCS` fixes how many Ps exist. That is the whole reason the answer is "no": the P is a *separate object from the thread*, so it can be moved when the thread gets stuck. ## What happens at the syscall boundary Every blocking call the standard library makes is bracketed by runtime hooks. On the way in, `entersyscall` records that the goroutine is leaving Go code and puts its P into a `_Psyscall` state — the P is still associated with this thread, but it is now up for grabs. For calls the runtime already knows will block for a long time, the standard library uses the stronger `entersyscallblock`, which releases the P right away (via `handoffp`) so another thread can take it without waiting for anyone to notice. For the ordinary path the runtime does **not** hand the P off eagerly, because most system calls return in well under a microsecond and a handoff is far more expensive than the call. Instead `sysmon`, a runtime-owned monitoring thread that runs without a P, sweeps the Ps periodically; if it finds one that has been in `_Psyscall` across one of its ticks (its fastest tick is on the order of tens of microseconds) and there is work that could use it, it retakes the P and hands it to another M. That M is taken from the runtime's list of parked threads if one is available, and created with `clone`/`pthread_create` if not. On the way out, `exitsyscall` runs. The fast path is that the goroutine's original P is still in `_Psyscall` and nobody stole it, so the same thread simply resumes with no scheduling work at all. If the P was taken, the runtime tries to acquire any idle P; failing that, the goroutine is put on the global run queue and its thread parks on the idle-M list, ready to be reused by the next blocking call. ## Why the thread itself cannot be saved Once control is inside the kernel, the thread belongs to the kernel. Go cannot preempt it, move it, or run anything else on it. So the currency of a blocking syscall is exactly one OS thread for its duration. That is the fundamental asymmetry to remember: **goroutines are cheap and the runtime protects them; threads are what a blocking call spends.** ## Network I/O is the exception Sockets are different. Go opens network file descriptors in non-blocking mode and registers them with the runtime's poller (epoll on Linux, kqueue on the BSDs and macOS, IOCP on Windows). A `Read` that has no data available does not enter a blocking syscall at all — the kernel returns `EAGAIN`, the runtime parks the goroutine on that descriptor, and the thread goes back to running other goroutines. When the poller reports the socket readable, the goroutine becomes runnable again. That is why a Go server can hold tens of thousands of connections on a handful of threads. ## cgo counts as a blocking call A call into C through cgo is treated by the runtime exactly like a system call: the thread is committed to the C code and the P can be handed to another M. So a slow C call also costs a thread, not the program's progress. ## The consequence you will actually observe Because each in-flight blocking call holds a thread, a process's OS thread count is not bounded by `GOMAXPROCS` and routinely exceeds it. A service with `GOMAXPROCS=4` doing many concurrent file or name-resolution calls can show dozens of threads. Nothing is broken — that is the design working — but each thread costs kernel bookkeeping and stack memory, so unbounded blocking concurrency is worth capping. ## The contrast worth having in your head In a runtime with no such handoff — a single-threaded event loop, or green threads pinned to one carrier thread — a blocking call really does stall every other task, and the standard advice is "never block the loop". Go removes that rule for correctness (your program still makes progress) but not for cost (you still pay a thread).

  • What happens to the goroutine and its thread when the blocking syscall finally returns?
    The runtime's exit-syscall path first tries to resume on the P the goroutine left, which is the common case and costs nothing. If that P was taken, it looks for any idle P; if there is none, the goroutine goes onto the global run queue and its OS thread parks on the runtime's idle-thread list, where the next blocking call will reuse it rather than creating a new thread.
  • Does every system call cost a hand-off of the P to another thread?
    No, and that matters for performance. Most calls return in well under a microsecond, so the runtime leaves the P attached in a syscall state and the same thread reclaims it on return with no scheduling work. Only calls the standard library flags as blocking, or calls still outstanding when `sysmon` next sweeps, actually trigger the hand-off.
  • Is a call into C through cgo handled the same way?
    Yes. The runtime brackets a cgo call like a system call: the thread is committed to the C code for its duration and the P can be handed to another thread, so the rest of the program keeps running. The cost is again one OS thread per in-flight call, which is why a slow C dependency shows up as thread growth rather than as a stall.

The P is the till; the M is the cashier. When the cashier walks into the stockroom (the kernel) and does not come back, the shop does not close — a colleague steps up to the same till and keeps serving the queue.

saying these in an interview costs you the question

  • Says a blocking syscall stops the whole Go program
  • Says the other goroutines on that P are stuck until it returns
  • Thinks GOMAXPROCS caps the number of OS threads
  • Claims Go makes every syscall non-blocking, files included
  • Confuses the blocked goroutine with a blocked P