// 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 pts

Implement:

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:

  • f must not run until the returned function is first called (lazy).
  • f runs exactly once, even when many goroutines call the returned function concurrently.
  • Every caller โ€” including callers that arrive while f is still running โ€” gets the value f returned, never a zero value.
  • Separate OnceValue wrappers are independent.

Log in to submit a solution and earn points.