Go

How do goroutines interact with cgo, and why can cgo calls be expensive?

Question 174HardGo 1.22 to 1.25

A cgo call switches from the goroutine's small, movable stack to the M's system (g0) stack, because C needs a large stack that does not move. The runtime treats the call like a syscall (entersyscall), so if it runs long, sysmon hands off the P. Each call costs tens of nanoseconds, compared with about 1 ns for a Go call. It also blocks a whole thread for the duration.

Consequences:

  • Many concurrent slow C calls mean many OS threads, possibly reaching the 10k limit.
  • The C code cannot be preempted. A goroutine in C counts as being in a syscall, so a GC stop-the-world does not wait for it, but if it returns to Go during a STW it blocks until the world restarts.
  • C code must not keep Go pointers after the call returns (cgo pointer rules; cheap checks are on by default with GODEBUG=cgocheck=1, full checks need GOEXPERIMENT=cgocheck2 since Go 1.21).
  • Callbacks from C threads into Go need an M. The runtime creates or borrows an "extra M", which adds cost.
  • A C library with thread-local state needs LockOSThread.

Mitigations:

  • Batch work so that each C call does more.
  • Limit concurrent C calls with a semaphore.
  • Prefer pure-Go libraries. The pure-Go DNS resolver, for example, avoids a thread per lookup.

What the interviewer wants to hear: "cgo is not Go". It changes how scheduling, memory and deployment behave.

More on Goroutines & the Scheduler

All 35 Goroutines & the Scheduler questions