Avoid strong reference reader count overflow - #131
Conversation
lorentey
left a comment
There was a problem hiding this comment.
Thank you! Interesting. Have you actually hit this overflow issue in practice?
The maximum reader count is 255 on platforms with 32-bit pointers, and 65535 on 64-bit platforms. We'd need hundreds/tens of thousands of distinct threads all trying to concurrently load the same reference to end up overflowing this counter.
I believe that would be difficult to impossible to achieve in practice -- for one thing, platforms typically have a lower system-wide maximum thread count. But, granted, this is an assumption that isn't checked anywhere; and it would make sense to enforce it. I believe the right move is for the code to trap on congestion overflow! We never expect that many readers to hammer the same reference all at once, so a runtime trap would be the right response if it does somehow happen.
| @_spi(Testing) | ||
| public static func _testStartLoadingReaderCounts( | ||
| for value: Value, | ||
| readerMask: UInt | ||
| ) -> (counts: [Int], overflowed: Bool) { |
There was a problem hiding this comment.
Let's not add test code to the public module. If we follow my recommendation that congestion overflow should result in a trap, then we (currently) have no good way to test that -- so the new test need to be removed, or adjusted to only exercise _startLoading up to, but not exceeding, the maximum reader count. (If you go with adjusting the test, move this code directly into a test method -- there is no need to define a utility function for it. Get rid of the configurable readerMask; use the real _readerMask.)
| } | ||
|
|
||
| private static func _finishLoading( | ||
| fileprivate static func _finishLoading( |
There was a problem hiding this comment.
| fileprivate static func _finishLoading( | |
| package static func _finishLoading( |
|
|
||
| @inline(__always) | ||
| fileprivate func _incrementingReaders( | ||
| readerMask: UInt = Self._readersMask | ||
| ) -> Self? { | ||
| precondition(readerMask <= Self._readersMask) | ||
| let readers = UInt(bitPattern: _readers) | ||
| guard readers < readerMask else { return nil } | ||
| return DoubleWord( | ||
| _raw: _raw, | ||
| readers: Int(readers + 1), | ||
| version: _version) | ||
| } |
There was a problem hiding this comment.
There is no real need for this; dw._readers += 1 will suffice.
| @inline(__always) | |
| fileprivate func _incrementingReaders( | |
| readerMask: UInt = Self._readersMask | |
| ) -> Self? { | |
| precondition(readerMask <= Self._readersMask) | |
| let readers = UInt(bitPattern: _readers) | |
| guard readers < readerMask else { return nil } | |
| return DoubleWord( | |
| _raw: _raw, | |
| readers: Int(readers + 1), | |
| version: _version) | |
| } |
| let r = UInt(bitPattern: readers) & Self._readersMask | ||
| assert(r == readers) | ||
| let r = UInt(bitPattern: readers) | ||
| precondition(r <= Self._readersMask) |
There was a problem hiding this comment.
Let's add a message to explain what happened:
| precondition(r <= Self._readersMask) | |
| precondition(r <= Self._readersMask, "Congestion overflow while loading an atomic reference") |
| let n = UInt(bitPattern: newValue) & Self._readersMask | ||
| assert(n == newValue) | ||
| let n = UInt(bitPattern: newValue) | ||
| precondition(n <= Self._readersMask) |
There was a problem hiding this comment.
| precondition(n <= Self._readersMask) | |
| precondition(n <= Self._readersMask, "Congestion overflow while loading an atomic reference") |
| @@ -166,17 +179,30 @@ internal struct _AtomicReferenceStorage { | |||
| from pointer: UnsafeMutablePointer<Self>, | |||
| hint: DoubleWord? = nil | |||
| ) -> DoubleWord { | |||
| var hint = hint | |||
| while true { | |||
| if let result = _tryStartLoading(from: pointer, hint: hint) { | |||
| return result | |||
| } | |||
| hint = nil | |||
| } | |||
| } | |||
|
|
|||
| fileprivate static func _tryStartLoading( | |||
| from pointer: UnsafeMutablePointer<Self>, | |||
| hint: DoubleWord? = nil, | |||
| readerMask: UInt = DoubleWord._readersMask | |||
| ) -> DoubleWord? { | |||
| var old = hint ?? Storage.atomicLoad(at: pointer._extract, ordering: .relaxed) | |||
| if old._raw == nil { | |||
| atomicMemoryFence(ordering: .acquiring) | |||
| return old | |||
| } | |||
| // Increment reader count | |||
| while true { | |||
| let new = DoubleWord( | |||
| _raw: old._raw, | |||
| readers: old._readers &+ 1, | |||
| version: old._version) | |||
| guard let new = old._incrementingReaders(readerMask: readerMask) else { | |||
| return nil | |||
| } | |||
| var done: Bool | |||
| (done, old) = Storage.atomicWeakCompareExchange( | |||
| expected: old, | |||
| @@ -189,7 +215,7 @@ internal struct _AtomicReferenceStorage { | |||
| } | |||
| } | |||
There was a problem hiding this comment.
| } | |
| package static func _startLoading( | |
| from pointer: UnsafeMutablePointer<Self>, | |
| hint: DoubleWord? = nil | |
| ) -> DoubleWord { | |
| var old = hint ?? Storage.atomicLoad(at: pointer._extract, ordering: .relaxed) | |
| if old._raw == nil { | |
| atomicMemoryFence(ordering: .acquiring) | |
| return old | |
| } | |
| // Increment reader count | |
| while true { | |
| let new = DoubleWord( | |
| _raw: old._raw, | |
| readers: old._readers &+ 1, | |
| version: old._version) | |
| var done: Bool | |
| (done, old) = Storage.atomicWeakCompareExchange( | |
| expected: old, | |
| desired: new, | |
| at: pointer._extract, | |
| successOrdering: .acquiring, | |
| failureOrdering: .acquiring) | |
| if done { return new } | |
| if old._raw == nil { return old } | |
| } | |
| } |
4285a49 to
ed49515
Compare
Summary
_startLoadingto reload and retry instead of publishing a wrapped count.Why
The strong-reference storage reserves a small reader-count field inside
DoubleWordand previously incremented it with wrapping arithmetic. If enough readers entered concurrently, the count could wrap and make the storage appear to have fewer active readers than it actually did.Testing
swift buildpasses locally.swift test --filter StrongReferenceRace.testReaderCountDoesNotWrapAtCapacitycould not run locally because this Swift toolchain cannot importXCTest.