Skip to content

Commit 1afc555

Browse files
committed
sync: fix deadlock in Map.Range callback
Currently, calling any sync.Map method from inside the sync.Map.Range callback f deadlocks. Moreover, Go's sync.Map explicitly permits the Range callback to call other methods on the map ("Range does not block other methods on the receiver; even f itself may call any method on m"). This commit prevents the deadlock by changing sync.Map.Range to: - take a snapshot of the map under the lock (maps.Clone) - release the lock - iterate over the snapshot A snapshot satisfies Go's sync.Map.Range contract, which only requires that no key is visited more than once and may reflect any mapping from any point during the call. Using a snapshot keeps the implementation simple, in line with this file's stated scope ("no more efficient than a map with a lock"). Also added TestMapRangeAndDelete regression test, which deletes map entries from inside the map's Range callback.
1 parent 801bd48 commit 1afc555

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

src/sync/map.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package sync
22

3-
import "internal/task"
3+
import (
4+
"internal/task"
5+
"maps"
6+
)
47

58
// This file implements just enough of sync.Map to get packages to compile. It
69
// is no more efficient than a map with a lock.
@@ -56,15 +59,20 @@ func (m *Map) Store(key, value interface{}) {
5659
m.m[key] = value
5760
}
5861

62+
// Range calls f for each key and value in the map. If f returns false, the iteration stops.
5963
func (m *Map) Range(f func(key, value interface{}) bool) {
60-
m.lock.Lock()
61-
defer m.lock.Unlock()
64+
// Iterate over a snapshot instead of holding the lock across the callback,
65+
// to prevent deadlock when a Map method is called inside f.
66+
//
67+
// Using a snapshot in Map.Range is sufficient because Go specifies that:
68+
// - Range only requires that no key is visited more than once, and
69+
// - Range may reflect any mapping from any point during the Range call.
6270

63-
if m.m == nil {
64-
return
65-
}
71+
m.lock.Lock()
72+
snapshot := maps.Clone(m.m)
73+
m.lock.Unlock()
6674

67-
for k, v := range m.m {
75+
for k, v := range snapshot {
6876
if !f(k, v) {
6977
break
7078
}

src/sync/map_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,31 @@ func TestMapSwap(t *testing.T) {
3636
t.Errorf("Load after Swap returned %v, %v, want foo, true", v, ok)
3737
}
3838
}
39+
40+
func TestMapRangeAndDelete(t *testing.T) {
41+
var sm sync.Map
42+
sm.Store(0, "0")
43+
sm.Store(1, "1")
44+
sm.Store(2, "2")
45+
46+
sm.Range(func(k, v any) bool {
47+
keyAsInt, ok := k.(int)
48+
if !ok {
49+
return true
50+
}
51+
if keyAsInt%2 == 0 {
52+
sm.Delete(keyAsInt)
53+
}
54+
return true
55+
})
56+
57+
if v, ok := sm.Load(0); ok {
58+
t.Errorf("Load(0) after Delete returned %v, %v, want nil, false", v, ok)
59+
}
60+
if v, ok := sm.Load(1); !ok || v.(string) != "1" {
61+
t.Errorf("Load(1) after Delete returned %v, %v, want \"1\", true", v, ok)
62+
}
63+
if v, ok := sm.Load(2); ok {
64+
t.Errorf("Load(2) after Delete returned %v, %v, want nil, false", v, ok)
65+
}
66+
}

0 commit comments

Comments
 (0)