Implement a readers-writer lock that prefers writers using only channels or sync.Mutex. Why is it hard to get right?
Question 529HardGo 1.22 to 1.25
"Writer-preferring" means that once a writer is waiting, new readers must block, even though the current readers keep going. Otherwise a steady stream of readers starves the writer forever. The version below uses a sync.Mutex plus sync.Cond, which is only a Mutex-guarded wait list.
type WPRWLock struct {
mu sync.Mutex
cond *sync.Cond
readers int // active readers
writing bool // a writer holds the lock
waitingWriters int
}
func NewWPRWLock() *WPRWLock {
l := &WPRWLock{}
l.cond = sync.NewCond(&l.mu)
return l
}
func (l *WPRWLock) RLock() {
l.mu.Lock()
for l.writing || l.waitingWriters > 0 { // yield to pending writers
l.cond.Wait()
}
l.readers++
l.mu.Unlock()
}
func (l *WPRWLock) RUnlock() {
l.mu.Lock()
if l.readers == 0 {
l.mu.Unlock()
panic("RUnlock of unlocked WPRWLock")
}
l.readers--
if l.readers == 0 {
l.cond.Broadcast()
}
l.mu.Unlock()
}
func (l *WPRWLock) Lock() {
l.mu.Lock()
l.waitingWriters++
for l.writing || l.readers > 0 {
l.cond.Wait()
}
l.waitingWriters--
l.writing = true
l.mu.Unlock()
}
func (l *WPRWLock) Unlock() {
l.mu.Lock()
l.writing = false
l.cond.Broadcast()
l.mu.Unlock()
}
Why it's hard:
- Starvation flips sides: writer preference can starve readers when writers keep arriving. Truly fair locks need phase alternation or FIFO tickets.
- Recursive read locking deadlocks: a goroutine holding RLock that calls RLock again blocks behind the pending writer, and the writer is waiting on that goroutine.
sync.RWMutexhas the same rule and documents that recursive read locking is forbidden. - Thundering herd:
Broadcastwakes everyone just to re-check, andSignalrisks waking the wrong class of waiter. Two separate Conds help, but they complicate the logic. - There is no cancellation or timeout, because Cond cannot be selected on.
- Misuse detection (unlocking an unlocked lock) and memory-ordering subtleties are easy to miss.
What the interviewer is looking for: knowing that the standard sync.RWMutex is already writer-preferring (a blocked Lock stops new RLock calls), and that you would use it in real code.
More on Classic Concurrency Coding Problems
- Q527Implement a debouncer and a throttler for a stream of events using time.Timer and channels.
- Q528Implement a sharded concurrent map (N shards with separate mutexes). When does it beat sync.Map and a single RWMutex?
- Q530Implement a retry-with-timeout helper that runs a function in a goroutine and returns early when the per-attempt or overall deadline expires, without leaking goroutines.
- Q531Implement a heartbeat/watchdog: a worker must signal liveness every T, or a supervisor restarts it.