RWMutex for Read-Heavy Access
sync.Mutex treats every access the same: Lock admits exactly one
goroutine, whether it's about to write or just wants to read. For a cache
that's read constantly and written rarely — configuration, a lookup table,
anything closer to "mostly read" than "mostly write" — that's needless
serialization. Two goroutines calling Get at the same time aren't in
conflict with each other; nothing about reading the same data twice at once
is unsafe. A plain Mutex doesn't know that, and makes the second reader
wait for the first anyway.
sync.RWMutex: two ways in
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
RLock/RUnlock is the read lock: any number of goroutines can hold it at
the same time, so concurrent Get calls run genuinely in parallel instead
of queuing. Lock/Unlock — the same pair sync.Mutex has — is the write
lock: exclusive, same as before, and it also blocks out every reader while
held. A writer can never run alongside a reader; only readers can run
alongside each other. That's the entire guarantee, and it's exactly the one
a map needs: concurrent reads are safe, a write concurrent with anything
else on the same data is not.
The rule for choosing between them
RWMutex isn't a strictly-better Mutex — it's usually slightly slower
under write-heavy or evenly-mixed access, because tracking "how many
readers are currently in" costs a little more than a plain mutex's binary
locked/unlocked state. It earns that cost back, and then some, exactly when
reads dominate writes by a wide margin: a config store read on every
request and written once at startup, a routing table read per lookup and
rebuilt occasionally. If you're not sure which shape your access pattern
has, sync.Mutex is the safer default — simpler, and never the wrong
choice for correctness, only sometimes the slower one under contention.
RWMutex is a lever you pull once profiling shows read contention
actually costing something, not a default to reach for on the first cache
you write.
Try it
Implement Cache's Get and Set using sync.RWMutex — reads use
RLock, writes use Lock, and both are safe to call concurrently from any
number of goroutines.
Practice
1. A Read-Heavy Cache
IntermediateImplement
NewCache,Cache.Get, andCache.Set.NewCache() *Cache— returns a ready-to-use, empty cache.Get(key string) (string, bool)— comma-ok lookup, using the read lock.Set(key, value string)— stores a value, using the exclusive write lock.
Both methods must be safe to call concurrently, from any number of goroutines, in any mix of reads and writes.
package main import "sync" type Cache struct { mu sync.RWMutex // TODO: add a field } // NewCache returns a ready-to-use, empty cache. func NewCache() *Cache { // TODO: implement return nil } // Get returns the value for key, and whether it was present. Safe for // concurrent use. func (c *Cache) Get(key string) (string, bool) { // TODO: implement return "", false } // Set stores value under key. Safe for concurrent use. func (c *Cache) Set(key, value string) { // TODO: implement } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.