// lesson: sync-beyond-mutex
sync Beyond Mutex
sync.Mutex is the workhorse, but the sync package has sharper tools,
each encoding a specific access pattern.
RWMutex splits lockers into readers and writers: any number of
readers may hold RLock simultaneously, while Lock waits for
exclusivity. Tracking "how many readers are in" costs more than a
Mutex's single bit of state โ a fixed cost paid whether or not any
real parallelism comes of it. The payoff comes from concurrent reader
traffic, not from the size of the critical section: four goroutines
genuinely overlapping on RLock around a single map lookup already
beat a plain Mutex, while the same lookup made by one or two goroutines
rarely does, however much work sits inside the lock. Benchmark at your
actual concurrency level before reaching for it.
Cond lets goroutines sleep until some condition over shared state might have changed. The non-negotiable idiom is checking the condition in a loop:
mu.Lock()
for !ready { // re-check: wakeups tell you to look, not that it's true
cond.Wait() // atomically unlocks mu, sleeps, relocks on wake
}
mu.Unlock()
Signal wakes one waiter, Broadcast wakes all; between the wakeup and
reacquiring the lock, another goroutine may have consumed the condition,
which is exactly why the loop is mandatory.
Once is the most interesting one, because its guarantee is about the
memory model, not just counting. once.Do(f) promises that f has
fully completed before any call to Do returns โ the completion of
f happens-before every return. That is what makes lazy initialization
safe to publish: whoever wins the race runs f, and everyone else
blocks until the result is visible. Note the trap this implies: a naive
if !done { mu.Lock(); ... } check outside the lock is a data race, and
the classic "double-checked locking" only works when the flag itself is
accessed atomically.
Go 1.21 added sync.OnceValue, which memoizes a function's result. You
will now build it yourself โ because knowing why the obvious versions
are wrong is the actual lesson.
โบ Build OnceValue
25 ptsImplement:
func OnceValue[T any](f func() T) func() T
without using sync.Once, sync.OnceValue, or sync.OnceValues โ
build the exactly-once machinery yourself with a mutex and/or atomics.
(The tests grade behavior, so delegating to the stdlib would pass the
grader โ and defeat the entire point of the exercise.)
Requirements:
fmust not run until the returned function is first called (lazy).fruns exactly once, even when many goroutines call the returned function concurrently.- Every caller โ including callers that arrive while
fis still running โ gets the valuefreturned, never a zero value. - Separate
OnceValuewrappers are independent.
Log in to submit a solution and earn points.