syncs: add Map.WithLock to allow mutations to the underlying map (#8101)

Some operations cannot be implemented with the prior API:
* Iterating over the map and deleting keys
* Iterating over the map and replacing items
* Calling APIs that expect a native Go map

Add a Map.WithLock method that acquires a write-lock on the map
and then calls a user-provided closure with the underlying Go map.
This allows users to interact with the Map as a regular Go map,
but with the gaurantees that it is concurrent safe.

Updates tailscale/corp#9115

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
pull/12791/head
Joe Tsai 5 months ago committed by GitHub
parent fc28c8e7f3
commit d209b032ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -252,8 +252,10 @@ func (m *Map[K, V]) Delete(key K) {
delete(m.m, key) delete(m.m, key)
} }
// Range iterates over the map in undefined order calling f for each entry. // Range iterates over the map in an undefined order calling f for each entry.
// Iteration stops if f returns false. Map changes are blocked during iteration. // Iteration stops if f returns false. Map changes are blocked during iteration.
// A read lock is held for the entire duration of the iteration.
// Use the [WithLock] method instead to mutate the map during iteration.
func (m *Map[K, V]) Range(f func(key K, value V) bool) { func (m *Map[K, V]) Range(f func(key K, value V) bool) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
@ -264,6 +266,15 @@ func (m *Map[K, V]) Range(f func(key K, value V) bool) {
} }
} }
// WithLock calls f with the underlying map.
// Use of m2 must not escape the duration of this call.
// The write-lock is held for the entire duration of this call.
func (m *Map[K, V]) WithLock(f func(m2 map[K]V)) {
m.mu.Lock()
defer m.mu.Unlock()
f(m.m)
}
// Len returns the length of the map. // Len returns the length of the map.
func (m *Map[K, V]) Len() int { func (m *Map[K, V]) Len() int {
m.mu.RLock() m.mu.RLock()

@ -7,7 +7,6 @@ import (
"context" "context"
"io" "io"
"os" "os"
"sync"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@ -189,19 +188,11 @@ func TestMap(t *testing.T) {
t.Run("LoadOrStore", func(t *testing.T) { t.Run("LoadOrStore", func(t *testing.T) {
var m Map[string, string] var m Map[string, string]
var wg sync.WaitGroup var wg WaitGroup
wg.Add(2)
var ok1, ok2 bool var ok1, ok2 bool
go func() { wg.Go(func() { _, ok1 = m.LoadOrStore("", "") })
defer wg.Done() wg.Go(func() { _, ok2 = m.LoadOrStore("", "") })
_, ok1 = m.LoadOrStore("", "")
}()
go func() {
defer wg.Done()
_, ok2 = m.LoadOrStore("", "")
}()
wg.Wait() wg.Wait()
if ok1 == ok2 { if ok1 == ok2 {
t.Errorf("exactly one LoadOrStore should load") t.Errorf("exactly one LoadOrStore should load")
} }

Loading…
Cancel
Save