sync: fix deadlock when a Map method is called from Map.Range - #5529
Conversation
|
Do we need to clone the entire map or can we get away with just getting a copy of all the keys and ranging over those and looking up values in the original map? |
@dgryski yes, we can snapshot just the keys under the lock and then Load each value from the Pros:
Cons:
Both approaches satisfy Go's Happy to switch to the key-snapshot approach if you prefer. Please let me know. |
1afc555 to
75f3342
Compare
|
I still think the lower memory usage from just copying the keys is worth it, on the assumption that uncontested locks are still pretty fast but memory pressure is expensive. |
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.
75f3342 to
c0e8924
Compare
dgryski
left a comment
There was a problem hiding this comment.
LGTM. Thanks for iterating on this!
Fixes #5528
Currently, calling any
sync.Mapmethod fromsync.Map.Rangecallbackfdeadlocks. Moreover, Go'ssync.Mapexplicitly permits theRangecallback 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 PR prevents the deadlock by changing
sync.Map.Rangeto:A snapshot satisfies Go's
sync.Map.Rangecontract, 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 with minimal changes to existing code, in line with this file's stated scope ("no more efficient than a map with a lock").
Deadlock Details
src/sync/map.go is a plain map guarded by a single non-reentrant
task.PMutex, and every method holds that lock for its entire duration.Rangelikewise holds the lock across the user callback, so anysync.Mapmethod called from inside the callback deadlocks.Added Tests
Added
TestMapRangeAndDeleteregression test, which deletes map entries from inside the map'sRangecallback.Tradeoffs
The snapshot approach prioritized simplicity, predictability, and minimal changes to existing code over performance.
See comment about copying entire map vs copying just the keys.
PR Updates
Updated to manually copy entries instead of using
maps.Clone. Initially, this PR usedmaps.Clone, but importingmapsfromsynctriggers animport cycle not allowederror on baremetal targets (e.g.-target=microbit) and breaksmake smoketest.Updated to copy only the keys as suggested by @dgryski. More details in my comment about copying entire map vs copying just the keys.