|
7 | 7 |
|
8 | 8 | import Foundation
|
9 | 9 |
|
10 |
| -// NOTE: Revised from previous implementation which used a struct and NSLock's. |
11 |
| -// Thread Sanitizer was *correctly* capturing this issue, which was a little obscure |
12 |
| -// given the property wrapper PLUS the semantics of a struct. Moving to `class` |
13 |
| -// removes the semantics problem and lets TSan approve of what's happening. |
14 |
| -// |
15 |
| -// Additionally, moving to a lock free version is just desirable, so moved to a queue. |
16 |
| -// |
17 |
| -// Also see thread here: https://github.com/apple/swift-evolution/pull/1387 |
| 10 | +/* |
| 11 | + Revised the implementation yet again. Tiziano Coriano noticed that this wrapper |
| 12 | + can be misleading about it's atomicity. A single set would be atomic, but a compound |
| 13 | + operation like += would cause an atomic read, and a separate atomic write, in which |
| 14 | + point another thread could've changed the value we're now working off of. |
| 15 | + |
| 16 | + This implementation removes the ability to set wrappedValue, and callers now must use |
| 17 | + the set() or mutate() functions explicitly to ensure a proper atomic mutation. |
| 18 | + |
| 19 | + The use of a dispatch queue was also removed in favor of an unfair lock (yes, it's |
| 20 | + implemented correctly). |
| 21 | + */ |
18 | 22 |
|
19 | 23 | @propertyWrapper
|
20 | 24 | public class Atomic<T> {
|
21 |
| - private var value: T |
22 |
| - private let queue = DispatchQueue(label: "com.segment.atomic.\(UUID().uuidString)") |
23 |
| - |
| 25 | + #if os(Linux) |
| 26 | + let swiftLock: NSLock |
| 27 | + #else |
| 28 | + internal typealias os_unfair_lock_t = UnsafeMutablePointer<os_unfair_lock_s> |
| 29 | + internal var unfairLock: os_unfair_lock_t |
| 30 | + #endif |
| 31 | + |
| 32 | + internal var value: T |
| 33 | + |
24 | 34 | public init(wrappedValue value: T) {
|
| 35 | + #if os(Linux) |
| 36 | + self.swiftLock = NSLock() |
| 37 | + #else |
| 38 | + self.unfairLock = UnsafeMutablePointer<os_unfair_lock_s>.allocate(capacity: 1) |
| 39 | + self.unfairLock.initialize(to: os_unfair_lock()) |
| 40 | + #endif |
25 | 41 | self.value = value
|
26 | 42 | }
|
27 |
| - |
| 43 | + |
| 44 | + deinit { |
| 45 | + #if !os(Linux) |
| 46 | + unfairLock.deallocate() |
| 47 | + #endif |
| 48 | + } |
| 49 | + |
28 | 50 | public var wrappedValue: T {
|
29 |
| - get { return queue.sync { return value } } |
30 |
| - set { queue.sync { value = newValue } } |
| 51 | + get { |
| 52 | + lock() |
| 53 | + defer { unlock() } |
| 54 | + return value |
| 55 | + } |
| 56 | + // set is not allowed, use set() or mutate() |
31 | 57 | }
|
| 58 | + |
| 59 | + public func set(_ newValue: T) { |
| 60 | + mutate { $0 = newValue } |
| 61 | + } |
| 62 | + |
| 63 | + public func mutate(_ mutation: (inout T) -> Void) { |
| 64 | + lock() |
| 65 | + defer { unlock() } |
| 66 | + mutation(&value) |
| 67 | + } |
| 68 | +} |
32 | 69 |
|
33 |
| - @discardableResult |
34 |
| - public func withValue(_ operation: (inout T) -> Void) -> T { |
35 |
| - queue.sync { |
36 |
| - operation(&self.value) |
37 |
| - return self.value |
38 |
| - } |
| 70 | +extension Atomic { |
| 71 | + internal func lock() { |
| 72 | + #if os(Linux) |
| 73 | + swiftLock.lock() |
| 74 | + #else |
| 75 | + os_unfair_lock_lock(unfairLock) |
| 76 | + #endif |
| 77 | + } |
| 78 | + |
| 79 | + internal func unlock() { |
| 80 | + #if os(Linux) |
| 81 | + swiftLock.unlock() |
| 82 | + #else |
| 83 | + os_unfair_lock_unlock(unfairLock) |
| 84 | + #endif |
39 | 85 | }
|
40 | 86 | }
|
0 commit comments