sync.RWMutex

A lock that admits many readers at once or one writer alone, and the cases where a plain Mutex is faster.

Syntaxvar mu sync.RWMutex; mu.RLock(); defer mu.RUnlock()

Most shared data is read far more often than it is changed. A sync.Mutex admits one goroutine at a time even when every one of them only wants to look. sync.RWMutex is a lock with two modes for that case. The read lock may be held by any number of goroutines at once. The write lock may be held by one goroutine only, and only while no reader holds the read lock.

How it works

RLock takes the read lock and RUnlock releases it. Lock takes the write lock and Unlock releases it. The read side is for code that only looks at the data. The write side is for code that changes it. The zero value is ready to use.

A goroutine waiting in Lock stops new readers from acquiring the read lock. That is deliberate: without it a steady stream of readers would keep a writer waiting forever.

When it is slower

An RWMutex does more bookkeeping than a Mutex, so each RLock costs more than a Lock. If the critical section is a few instructions, or if readers are not actually concurrent, the plain Mutex wins. Write a benchmark for both and keep the faster one. Start with sync.Mutex and change only when a measurement asks you to.

What trips people up

The read lock permits reading only. Changing anything under RLock is still a data race, and the race detector will say so. Also remember that holding the read lock does not reserve a place in the queue for the write lock.

Example

Example · go
package main

import (
	"fmt"
	"sync"
)

// store holds settings that are read often and written rarely.
type store struct {
	mu sync.RWMutex
	m  map[string]string
}

// get takes the read lock, so several gets may run at the same time.
func (s *store) get(key string) (string, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	v, ok := s.m[key]
	return v, ok
}

// set takes the write lock, which excludes every reader.
func (s *store) set(key, value string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.m[key] = value
}

func main() {
	s := &store{m: make(map[string]string)}
	s.set("region", "eu-west-1")
	var wg sync.WaitGroup
	for range 3 {
		wg.Go(func() {
			v, _ := s.get("region")
			fmt.Println("region:", v)
		})
	}
	wg.Wait()
}

// Output:
// region: eu-west-1
// region: eu-west-1
// region: eu-west-1

When to use it

  • A configuration or feature-flag holder read by every request and replaced once a minute.
  • An in-memory cache of database rows where reads vastly outnumber writes.
  • A routing table or client registry that many goroutines consult and one background goroutine updates.

More examples

Filling a missing entry without holding the write lock the whole time

There is no way to turn a read lock into a write lock. You release one and take the other, and in that gap another goroutine may have done the same work. The second check inside the write lock is what makes that safe. This shape is the core of a read-mostly cache; reach for it whenever the miss path is slow and the hit path must stay parallel.

Example · go
func (s *store) load(key string) string {
	s.mu.RLock()
	v, ok := s.m[key]
	s.mu.RUnlock()
	if ok {
		return v
	}

	// Slow work happens with no lock held at all.
	v = computeValue(key)

	s.mu.Lock()
	defer s.mu.Unlock()
	// Another goroutine may have filled the key while we computed.
	// Check again and keep whichever value is already there.
	if existing, ok := s.m[key]; ok {
		return existing
	}
	s.m[key] = v
	return v
}

RLocker, for code that wants a plain lock

<code>sync.Locker</code> is an interface with just <code>Lock</code> and <code>Unlock</code>. Some helpers accept one. <code>RLocker</code> hands them the read side of your RWMutex without them knowing there is a write side. Use it to pass a read lock into such a helper; do not use it to hide from yourself which side you are on.

Example · go
type report struct {
	mu   sync.RWMutex
	rows []string
}

func (r *report) render() string {
	// RLocker returns a sync.Locker whose Lock and Unlock call
	// RLock and RUnlock. Useful for code that takes a Locker.
	var l sync.Locker = r.mu.RLocker()
	l.Lock()
	defer l.Unlock()
	return fmt.Sprint(len(r.rows), " rows")
}

Deciding between Mutex and RWMutex with a benchmark

<code>b.RunParallel</code> runs the body on several goroutines at once, which is the only condition under which an RWMutex can win. Run <code>go test -bench=Get</code> on both and compare. On a one-line critical section the Mutex often comes out ahead, which is the honest answer to "should I use RWMutex here".

Example · go
func BenchmarkGetMutex(b *testing.B) {
	s := newMutexStore()
	s.set("k", "v")
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			s.get("k")
		}
	})
}

func BenchmarkGetRWMutex(b *testing.B) {
	s := newRWStore()
	s.set("k", "v")
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			s.get("k")
		}
	})
}

Discussion

  • Be the first to comment on this lesson.