Skip to content

Commit cf5bed8

Browse files
fxamackerdgryski
authored andcommitted
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: - copy the map's keys under the lock - release the lock - iterate over the key 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 babdfc9 commit cf5bed8

2 files changed

Lines changed: 48 additions & 8 deletions

File tree

src/sync/map.go

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

3-
import "internal/task"
3+
import (
4+
"internal/task"
5+
)
46

57
// This file implements just enough of sync.Map to get packages to compile. It
68
// is no more efficient than a map with a lock.
@@ -56,17 +58,27 @@ func (m *Map) Store(key, value interface{}) {
5658
m.m[key] = value
5759
}
5860

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

63-
if m.m == nil {
64-
return
70+
m.lock.Lock()
71+
keys := make([]interface{}, 0, len(m.m))
72+
for k := range m.m {
73+
keys = append(keys, k)
6574
}
75+
m.lock.Unlock()
6676

67-
for k, v := range m.m {
68-
if !f(k, v) {
69-
break
77+
for _, k := range keys {
78+
if v, ok := m.Load(k); ok {
79+
if !f(k, v) {
80+
break
81+
}
7082
}
7183
}
7284
}

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)