Skip to content

Commit d78473b

Browse files
authored
Refactor ThreadSafeBox to use non-optional type (#9477)
Change the underlying storage type from `Value?` to storing `Value`, with optional-specific operations moved to constrained extensions. This improves type safety by eliminating unnecessary optionality for non-optional types. This stronger type constraint makes it more difficult for the struct to be misused. For instance in the current implementation the extension method `increment()` will fail to increment if the user forgot to initialize the `ThreadSafeBox` with a value. Now a value must be set initially before increment is called. Also this patch prevents a race condition in the `memoize` methods where the call to the memoized `body` was not guarded by a lock which allowed multiple threads to call memoize at once and produce inconsistent results. Finally, fix the subscript setter so it works properly with `structs`, as the `if var value` and then value set will set the value on a copy, not on the `self.underlying` struct. Now that the type is no longer optional this unwrapping isn't necessary and we can set directly on the value.
1 parent 736369d commit d78473b

24 files changed

+1136
-174
lines changed

Sources/Basics/Cancellator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public final class Cancellator: Cancellable, Sendable {
9999

100100
@discardableResult
101101
public func register(name: String, handler: @escaping CancellationHandler) -> RegistrationKey? {
102-
if self.cancelling.get(default: false) {
102+
if self.cancelling.get() {
103103
self.observabilityScope?.emit(debug: "not registering '\(name)' with terminator, termination in progress")
104104
return .none
105105
}

Sources/Basics/Concurrency/AsyncProcess.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ package final class AsyncProcess {
806806
package func waitUntilExit() throws -> AsyncProcessResult {
807807
let group = DispatchGroup()
808808
group.enter()
809-
let resultBox = ThreadSafeBox<Result<AsyncProcessResult, Swift.Error>>()
809+
let resultBox = ThreadSafeBox<Result<AsyncProcessResult, Swift.Error>?>()
810810
self.waitUntilExit { result in
811811
resultBox.put(result)
812812
group.leave()

Sources/Basics/Concurrency/ConcurrencyHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public enum Concurrency {
3030
public func unsafe_await<T: Sendable>(_ body: @Sendable @escaping () async -> T) -> T {
3131
let semaphore = DispatchSemaphore(value: 0)
3232

33-
let box = ThreadSafeBox<T>()
33+
let box = ThreadSafeBox<T?>()
3434
Task {
3535
let localValue: T = await body()
3636
box.mutate { _ in localValue }

Sources/Basics/Concurrency/ThreadSafeBox.swift

Lines changed: 135 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -12,135 +12,211 @@
1212

1313
import class Foundation.NSLock
1414

15-
/// Thread-safe value boxing structure
15+
/// Thread-safe value boxing structure that provides synchronized access to a wrapped value.
1616
@dynamicMemberLookup
1717
public final class ThreadSafeBox<Value> {
18-
private var underlying: Value?
18+
private var underlying: Value
1919
private let lock = NSLock()
2020

21-
public init() {}
22-
21+
/// Creates a new thread-safe box with the given initial value.
22+
///
23+
/// - Parameter seed: The initial value to store in the box.
2324
public init(_ seed: Value) {
2425
self.underlying = seed
2526
}
2627

27-
public func mutate(body: (Value?) throws -> Value?) rethrows {
28+
/// Atomically mutates the stored value by applying a transformation function.
29+
///
30+
/// The transformation function receives the current value and returns a new value
31+
/// to replace it. The entire operation is performed under a lock to ensure atomicity.
32+
///
33+
/// - Parameter body: A closure that takes the current value and returns a new value.
34+
/// - Throws: Any error thrown by the transformation function.
35+
public func mutate(body: (Value) throws -> Value) rethrows {
2836
try self.lock.withLock {
2937
let value = try body(self.underlying)
3038
self.underlying = value
3139
}
3240
}
33-
34-
public func mutate(body: (inout Value?) throws -> ()) rethrows {
41+
42+
/// Atomically mutates the stored value by applying an in-place transformation.
43+
///
44+
/// The transformation function receives an inout reference to the current value,
45+
/// allowing direct modification. The entire operation is performed under a lock
46+
/// to ensure atomicity.
47+
///
48+
/// - Parameter body: A closure that receives an inout reference to the current value.
49+
/// - Throws: Any error thrown by the transformation function.
50+
public func mutate(body: (inout Value) throws -> Void) rethrows {
3551
try self.lock.withLock {
3652
try body(&self.underlying)
3753
}
3854
}
3955

40-
@discardableResult
41-
public func memoize(body: () throws -> Value) rethrows -> Value {
42-
if let value = self.get() {
43-
return value
44-
}
45-
let value = try body()
56+
/// Atomically retrieves the current value from the box.
57+
///
58+
/// - Returns: A copy of the current value stored in the box.
59+
public func get() -> Value {
4660
self.lock.withLock {
47-
self.underlying = value
61+
self.underlying
4862
}
49-
return value
5063
}
5164

52-
@discardableResult
53-
public func memoize(body: () async throws -> Value) async rethrows -> Value {
54-
if let value = self.get() {
55-
return value
56-
}
57-
let value = try await body()
65+
/// Atomically replaces the current value with a new value.
66+
///
67+
/// - Parameter newValue: The new value to store in the box.
68+
public func put(_ newValue: Value) {
5869
self.lock.withLock {
59-
self.underlying = value
70+
self.underlying = newValue
6071
}
61-
return value
6272
}
6373

64-
public func clear() {
74+
/// Provides thread-safe read-only access to properties of the wrapped value.
75+
///
76+
/// This subscript allows you to access properties of the wrapped value using
77+
/// dot notation while maintaining thread safety.
78+
///
79+
/// - Parameter keyPath: A key path to a property of the wrapped value.
80+
/// - Returns: The value of the specified property.
81+
public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
6582
self.lock.withLock {
66-
self.underlying = nil
83+
self.underlying[keyPath: keyPath]
6784
}
6885
}
6986

70-
public func get() -> Value? {
71-
self.lock.withLock {
72-
self.underlying
87+
/// Provides thread-safe read-write access to properties of the wrapped value.
88+
///
89+
/// - Parameter keyPath: A writable key path to a property of the wrapped value.
90+
/// - Returns: The value of the specified property when getting.
91+
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
92+
get {
93+
self.lock.withLock {
94+
self.underlying[keyPath: keyPath]
95+
}
96+
}
97+
set {
98+
self.lock.withLock {
99+
self.underlying[keyPath: keyPath] = newValue
100+
}
73101
}
74102
}
103+
}
75104

76-
public func get(default: Value) -> Value {
77-
self.lock.withLock {
78-
self.underlying ?? `default`
79-
}
105+
// Extension for optional values to support empty initialization
106+
extension ThreadSafeBox {
107+
/// Creates a new thread-safe box initialized with nil for optional value types.
108+
///
109+
/// This convenience initializer is only available when the wrapped value type is optional.
110+
public convenience init<Wrapped>() where Value == Wrapped? {
111+
self.init(nil)
80112
}
81113

82-
public func put(_ newValue: Value) {
114+
/// Takes the stored optional value, setting it to nil.
115+
/// - Returns: The previously stored value, or nil if none was present.
116+
public func takeValue<Wrapped>() -> Value where Value == Wrapped? {
83117
self.lock.withLock {
84-
self.underlying = newValue
118+
guard let value = self.underlying else { return nil }
119+
self.underlying = nil
120+
return value
85121
}
86122
}
87123

88-
public func takeValue<U>() -> Value where U? == Value {
124+
/// Atomically sets the stored optional value to nil.
125+
///
126+
/// This method is only available when the wrapped value type is optional.
127+
public func clear<Wrapped>() where Value == Wrapped? {
89128
self.lock.withLock {
90-
guard let value = self.underlying else { return nil }
91129
self.underlying = nil
92-
return value
93130
}
94131
}
95132

96-
public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T? {
133+
/// Atomically retrieves the stored value, returning a default if nil.
134+
///
135+
/// This method is only available when the wrapped value type is optional.
136+
///
137+
/// - Parameter defaultValue: The value to return if the stored value is nil.
138+
/// - Returns: The stored value if not nil, otherwise the default value.
139+
public func get<Wrapped>(default defaultValue: Wrapped) -> Wrapped where Value == Wrapped? {
97140
self.lock.withLock {
98-
self.underlying?[keyPath: keyPath]
141+
self.underlying ?? defaultValue
99142
}
100143
}
101144

102-
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T?>) -> T? {
103-
get {
104-
self.lock.withLock {
105-
self.underlying?[keyPath: keyPath]
145+
/// Atomically computes and caches a value if not already present.
146+
///
147+
/// If the box already contains a non-nil value, that value is returned immediately.
148+
/// Otherwise, the provided closure is executed to compute the value, which is then
149+
/// stored and returned. This method is only available when the wrapped value type is optional.
150+
///
151+
/// - Parameter body: A closure that computes the value to store if none exists.
152+
/// - Returns: The cached value or the newly computed value.
153+
/// - Throws: Any error thrown by the computation closure.
154+
@discardableResult
155+
public func memoize<Wrapped>(body: () throws -> Wrapped) rethrows -> Wrapped where Value == Wrapped? {
156+
try self.lock.withLock {
157+
if let value = self.underlying {
158+
return value
106159
}
160+
let value = try body()
161+
self.underlying = value
162+
return value
107163
}
108-
set {
109-
self.lock.withLock {
110-
if var value = self.underlying {
111-
value[keyPath: keyPath] = newValue
112-
}
164+
}
165+
166+
/// Atomically computes and caches an optional value if not already present.
167+
///
168+
/// If the box already contains a non-nil value, that value is returned immediately.
169+
/// Otherwise, the provided closure is executed to compute the value, which is then
170+
/// stored and returned. This method is only available when the wrapped value type is optional.
171+
///
172+
/// If the returned value is `nil` subsequent calls to `memoize` or `memoizeOptional` will
173+
/// re-execute the closure.
174+
///
175+
/// - Parameter body: A closure that computes the optional value to store if none exists.
176+
/// - Returns: The cached value or the newly computed value (which may be nil).
177+
/// - Throws: Any error thrown by the computation closure.
178+
@discardableResult
179+
public func memoizeOptional<Wrapped>(body: () throws -> Wrapped?) rethrows -> Wrapped? where Value == Wrapped? {
180+
try self.lock.withLock {
181+
if let value = self.underlying {
182+
return value
113183
}
184+
let value = try body()
185+
self.underlying = value
186+
return value
114187
}
115188
}
116189
}
117190

118191
extension ThreadSafeBox where Value == Int {
192+
/// Atomically increments the stored integer value by 1.
193+
///
194+
/// This method is only available when the wrapped value type is Int.
119195
public func increment() {
120196
self.lock.withLock {
121-
if let value = self.underlying {
122-
self.underlying = value + 1
123-
}
197+
self.underlying = self.underlying + 1
124198
}
125199
}
126200

201+
/// Atomically decrements the stored integer value by 1.
202+
///
203+
/// This method is only available when the wrapped value type is Int.
127204
public func decrement() {
128205
self.lock.withLock {
129-
if let value = self.underlying {
130-
self.underlying = value - 1
131-
}
206+
self.underlying = self.underlying - 1
132207
}
133208
}
134209
}
135210

136211
extension ThreadSafeBox where Value == String {
212+
/// Atomically appends a string to the stored string value.
213+
///
214+
/// This method is only available when the wrapped value type is String.
215+
///
216+
/// - Parameter value: The string to append to the current stored value.
137217
public func append(_ value: String) {
138218
self.mutate { existingValue in
139-
if let existingValue {
140-
return existingValue + value
141-
} else {
142-
return value
143-
}
219+
existingValue + value
144220
}
145221
}
146222
}

Sources/Basics/Observability.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
163163
}
164164

165165
var errorsReported: Bool {
166-
self._errorsReported.get() ?? false
166+
self._errorsReported.get()
167167
}
168168
}
169169
}

Sources/Build/BuildOperation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,10 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
171171
}
172172

173173
/// The build description resulting from planing.
174-
private let buildDescription = ThreadSafeBox<BuildDescription>()
174+
private let buildDescription = AsyncThrowingValueMemoizer<BuildDescription>()
175175

176176
/// The loaded package graph.
177-
private let packageGraph = ThreadSafeBox<ModulesGraph>()
177+
private let packageGraph = AsyncThrowingValueMemoizer<ModulesGraph>()
178178

179179
/// File system to operate on.
180180
private var fileSystem: Basics.FileSystem {

Sources/Build/ClangSupport.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public enum ClangSupport {
2424
let features: [Feature]
2525
}
2626

27-
private static var cachedFeatures = ThreadSafeBox<Features>()
27+
private static var cachedFeatures = ThreadSafeBox<Features?>()
2828

2929
public static func supportsFeature(name: String, toolchain: PackageModel.Toolchain) throws -> Bool {
3030
let features = try cachedFeatures.memoize {

Sources/Build/LLBuildProgressTracker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public final class BuildExecutionContext {
9696

9797
// MARK: - Private
9898

99-
private var indexStoreAPICache = ThreadSafeBox<Result<IndexStoreAPI, Error>>()
99+
private var indexStoreAPICache = ThreadSafeBox<Result<IndexStoreAPI, Error>?>()
100100

101101
/// Reference to the index store API.
102102
var indexStoreAPI: Result<IndexStoreAPI, Error> {

Sources/Commands/SwiftTestCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1223,7 +1223,7 @@ final class ParallelTestRunner {
12231223
}
12241224
self.finishedTests.enqueue(TestResult(
12251225
unitTest: test,
1226-
output: output.get() ?? "",
1226+
output: output.get(),
12271227
success: result != .failure,
12281228
duration: duration
12291229
))

Sources/DriverSupport/DriverSupportUtils.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import class TSCBasic.Process
1919
import struct TSCBasic.ProcessResult
2020

2121
public enum DriverSupport {
22-
private static var flagsMap = ThreadSafeBox<[String: Set<String>]>()
22+
private static var flagsMap = ThreadSafeBox<[String: Set<String>]>([:])
2323

2424
/// This checks _frontend_ supported flags, which are not necessarily supported in the driver.
2525
public static func checkSupportedFrontendFlags(
@@ -29,8 +29,9 @@ public enum DriverSupport {
2929
) -> Bool {
3030
let trimmedFlagSet = Set(flags.map { $0.trimmingCharacters(in: ["-"]) })
3131
let swiftcPathString = toolchain.swiftCompilerPath.pathString
32+
let entry = flagsMap.get()
3233

33-
if let entry = flagsMap.get(), let cachedSupportedFlagSet = entry[swiftcPathString + "-frontend"] {
34+
if let cachedSupportedFlagSet = entry[swiftcPathString + "-frontend"] {
3435
return cachedSupportedFlagSet.intersection(trimmedFlagSet) == trimmedFlagSet
3536
}
3637
do {
@@ -63,8 +64,9 @@ public enum DriverSupport {
6364
) -> Bool {
6465
let trimmedFlagSet = Set(flags.map { $0.trimmingCharacters(in: ["-"]) })
6566
let swiftcPathString = toolchain.swiftCompilerPath.pathString
67+
let entry = flagsMap.get()
6668

67-
if let entry = flagsMap.get(), let cachedSupportedFlagSet = entry[swiftcPathString + "-driver"] {
69+
if let cachedSupportedFlagSet = entry[swiftcPathString + "-driver"] {
6870
return cachedSupportedFlagSet.intersection(trimmedFlagSet) == trimmedFlagSet
6971
}
7072
do {

0 commit comments

Comments
 (0)