// lesson: parking-and-wakeups
Parking and Wakeups
What actually happens when a goroutine blocks โ on a channel receive, a mutex, a timer? The one thing that must not happen is an OS thread going to sleep or, worse, spinning. Threads are the scarce resource; blocking one on every blocked goroutine would collapse M:N scheduling back to 1:1.
The runtime's answer is a pair of functions you'll see all over proc.go:
goparkโ called by the blocking goroutine, on its own stack. It flips the G's status from_Grunningto_Gwaiting, records awaitreason(that's the "chan receive" you see in goroutine dumps), then switches to the scheduler viamcall. Crucially, the G is now on no run queue at all. It is not scheduled, not polled, costs zero CPU. It lives only in whatever wait-list the blocking primitive keeps โ a channel's waiter queue, a mutex's semaphore list โ as asudog(a small "G waiting on this thing" node; one G can sit in several wait-lists at once thanks toselect).goreadyโ called by whoever unblocks it (the sender, the unlocker). It flips the G back to_Grunnableand pushes it onto a run queue. The M never stopped: aftergoparkit immediately picked up another runnable G.
sync.Mutex is a consumer of this machinery: its fast path is a single
atomic CAS (compare-and-swap: "if the value is still what I last saw, swap
in the new one โ atomically, in one CPU instruction, no lock needed") in
userspace, and only the contended slow path calls into the runtime
semaphore (semacquire/semrelease), which is a futex-style "sleep until
someone releases" built on gopark/goready.
You can't call gopark from user code โ but you don't need to, because
a channel operation is a gopark/goready pair in a trench coat. A blocked
ch <- x parks you on the channel's sender wait-list; a receive readies you,
FIFO. So a buffered channel of capacity 1 is a complete lock implementation:
Lock= put the token in (ch <- struct{}{}) โ blocks (parks!) while the buffer is full, i.e. while someone else holds the lock.Unlock= take the token out (<-ch) โ wakes exactly one parked waiter.TryLock= aselectwith adefaultarm: the non-blocking attempt.
Fairness note: waiters on a channel are queued FIFO by the runtime, which is
a nicer property than sync.Mutex guarantees in its normal (barging) mode.
You get that for free.
โบ A Mutex from a Channel
25 ptsImplement a mutex whose entire blocking behavior is delegated to the runtime's channel park/unpark machinery:
type Mutex struct { /* your state โ a chan struct{} of capacity 1 */ }
func NewMutex() *Mutex
func (m *Mutex) Lock() // parks until the lock is available
func (m *Mutex) TryLock() bool // never blocks; true iff it acquired the lock
func (m *Mutex) Unlock() // releases; wakes one parked Lock, FIFO
Required semantics:
- Mutual exclusion: between
Lock(or a successfulTryLock) and the matchingUnlock, no otherLock/TryLockmay succeed. TryLockreturns immediately:trueand holds the lock, orfalse.Unlockof an unlocked Mutex must panic (with any message). Detect it with the non-blocking receive pattern โ if there's no token to remove, the mutex wasn't locked. Like the realsync.Mutex, there is no ownership: any goroutine may unlock a locked mutex.- Do not use
sync.Mutex/sync.RWMutexor spin loops; the point is that one buffered channel already is the lock. (Honor system again.)
The tests hammer it from 8 goroutines with a critical-section collision detector: every entry does an atomic increment of an "occupancy" gauge and fails if it ever reads anything but 1.
Log in to submit a solution and earn points.