Go

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.RWMutex has the same rule and documents that recursive read locking is forbidden.
  • Thundering herd: Broadcast wakes everyone just to re-check, and Signal risks 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

All 16 Classic Concurrency Coding Problems questions