From 391ffc330adc8c9d0f77908fb88fa1e9ee644658 Mon Sep 17 00:00:00 2001 From: Aleksey Berezka Date: Thu, 30 Apr 2026 18:42:39 +0500 Subject: [PATCH 1/4] feat: add Mutex-compatible withLock API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 20 ++++++++ .../DBThreadSafe/DBThreadSafeContainer.swift | 11 +++-- .../DBThreadSafeContainerTests.swift | 49 +++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e56f60b..cab7371 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,26 @@ To create a new instance of DBThreadSafeContainer, simply initialize it with an let container = DBThreadSafeContainer("Hello, World!") ``` +### Mutex-compatible access + +If you plan to migrate from `DBThreadSafeContainer` to Apple's `Mutex`, prefer `withLock`: + +```swift +let length = container.withLock { value in + value.count +} +``` + +`withLock` also supports mutation through `inout`, matching the call-site shape of `Mutex`: + +```swift +container.withLock { value in + value += "!" +} +``` + +The existing `read` and `write` APIs remain available. + ### Reading the value To read the value stored in the container, use the `read()` method: diff --git a/Sources/DBThreadSafe/DBThreadSafeContainer.swift b/Sources/DBThreadSafe/DBThreadSafeContainer.swift index 2fee5d1..958a5d7 100644 --- a/Sources/DBThreadSafe/DBThreadSafeContainer.swift +++ b/Sources/DBThreadSafe/DBThreadSafeContainer.swift @@ -27,6 +27,13 @@ public final class DBThreadSafeContainer: Sendable { defer { lock.unlock() } return try closure(value) } + + /// Executes a closure while holding an exclusive lock on the stored value. + public func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + lock.writeLock() + defer { lock.unlock() } + return try closure(&value) + } /// Replaces current value with a new one /// - Parameter newValue: The new value to be stored in the container. @@ -38,8 +45,6 @@ public final class DBThreadSafeContainer: Sendable { /// Returns current value in a closure with possibility to make multiple modifications of any kind inside a single lock. public func write(_ closure: (_ value: inout T) throws -> Void) rethrows { - lock.writeLock() - defer { lock.unlock() } - try closure(&value) + try withLock(closure) } } diff --git a/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift b/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift index 9e5c1e1..830226f 100644 --- a/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift +++ b/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift @@ -126,4 +126,53 @@ struct DBThreadSafeContainerTests { #expect(container.read() == ["key": iterations]) } + // MARK: - Mutex-compatible API + + @Test("withLock returns transformed value") + func withLockReturnValue() { + let container = DBThreadSafeContainer("Hello, World!") + + let length = container.withLock { value in + value.count + } + + #expect(length == 13) + } + + @Test("withLock provides inout access for mutation") + func withLockMutation() { + let container = DBThreadSafeContainer(0) + + container.withLock { value in + value = 42 + } + + #expect(container.read() == 42) + } + + @Test("Concurrent increments via withLock") + func concurrentWithLock() { + let container = DBThreadSafeContainer(0) + + DispatchQueue.concurrentPerform(iterations: iterations) { _ in + container.withLock { value in + value += 1 + } + } + + #expect(container.read() == iterations) + } + + @Test("withLock rethrows from throwing closure") + func withLockThrowing() { + let container = DBThreadSafeContainer(0) + + enum TestError: Error { case someError } + + #expect(throws: TestError.self) { + try container.withLock { _ in + throw TestError.someError + } + } + } } From 1e9cb00c32206d75fceb28a0ee0f40d5afd39b22 Mon Sep 17 00:00:00 2001 From: Aleksey Berezka Date: Thu, 30 Apr 2026 20:05:24 +0500 Subject: [PATCH 2/4] feat: add selectable mutex backend Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 42 +++++- .../DBThreadSafe/DBThreadSafeContainer.swift | 59 ++++---- Sources/DBThreadSafe/DBThreadSafeLock.swift | 11 ++ Sources/DBThreadSafe/Lock.swift | 127 +++++++++++++++++- Sources/DBThreadSafe/ThreadSafe.swift | 5 + .../DBThreadSafeContainerTests.swift | 36 +++++ Tests/DBThreadSafeTests/ThreadSafeTests.swift | 27 ++++ 7 files changed, 273 insertions(+), 34 deletions(-) create mode 100644 Sources/DBThreadSafe/DBThreadSafeLock.swift diff --git a/README.md b/README.md index cab7371..b717ca0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdodobrands%2FDBThreadSafe-ios%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/dodobrands/DBThreadSafe-ios) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdodobrands%2FDBThreadSafe-ios%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/dodobrands/DBThreadSafe-ios) -DBThreadSafeContainer is a generic class that provides thread-safe read and write access to a stored value. It uses a `pthread_rwlock_t` lock to ensure that multiple threads can safely access the value concurrently. +DBThreadSafeContainer is a generic class that provides thread-safe read and write access to a stored value. By default it uses a `pthread_rwlock_t` backend and can opt into Apple's `Synchronization.Mutex` on supported OS versions. ## Usage @@ -17,6 +17,37 @@ To create a new instance of DBThreadSafeContainer, simply initialize it with an let container = DBThreadSafeContainer("Hello, World!") ``` +The default initializer keeps the `pthread_rwlock_t` backend for backwards-compatible read semantics: + +You can inspect the chosen backend through `lockType`: + +```swift +let container = DBThreadSafeContainer("Hello, World!") +let lockType = container.lockType // .pthreadRWLock +``` + +### Selecting a lock backend explicitly + +Use `DBThreadSafeLock` to force a specific backend: + +```swift +let pthreadContainer = DBThreadSafeContainer("Hello, World!", lock: .pthreadRWLock) +``` + +`Synchronization.Mutex` can only be selected on supported platforms: + +```swift +if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { + let mutexContainer = DBThreadSafeContainer("Hello, World!", lock: .mutex) +} +``` + +The same selection API is available on the property wrapper: + +```swift +@ThreadSafe(lock: .pthreadRWLock) var counter = 0 +``` + ### Mutex-compatible access If you plan to migrate from `DBThreadSafeContainer` to Apple's `Mutex`, prefer `withLock`: @@ -101,11 +132,16 @@ try container.write { value in ## Thread Safety -DBThreadSafeContainer ensures that read and write operations are thread-safe by using a `pthread_rwlock_t` lock. This allows multiple threads to read the value concurrently, while ensuring that only one thread can write to the value at a time. +DBThreadSafeContainer ensures that read and write operations are thread-safe, but the exact semantics depend on the selected backend: + +- `pthread_rwlock_t`: multiple readers can proceed concurrently, while writes remain exclusive +- `Synchronization.Mutex`: both reads and writes are exclusive critical sections + +The default initializer preserves concurrent reader behavior. If you explicitly choose `Synchronization.Mutex`, reads become exclusive critical sections just like writes. ## Cleanup -DBThreadSafeContainer automatically destroys the `pthread_rwlock_t` lock when it is deallocated to prevent any resource leaks. +DBThreadSafeContainer automatically cleans up the selected lock backend when it is deallocated. ## License diff --git a/Sources/DBThreadSafe/DBThreadSafeContainer.swift b/Sources/DBThreadSafe/DBThreadSafeContainer.swift index 958a5d7..b06df9e 100644 --- a/Sources/DBThreadSafe/DBThreadSafeContainer.swift +++ b/Sources/DBThreadSafe/DBThreadSafeContainer.swift @@ -1,50 +1,59 @@ import Foundation public final class DBThreadSafeContainer: Sendable { - nonisolated(unsafe) private var value: T - private let lock = Lock() - + private let storage: LockStorage + + /// The concrete lock backend currently used by the container. + public var lockType: DBThreadSafeLock { + storage.lockType + } + public init(_ value: T) { - self.value = value + self.storage = PThreadRWLockStorage(value) + } + + public init(_ value: T, lock: DBThreadSafeLock) { + switch lock { + case .pthreadRWLock: + self.storage = PThreadRWLockStorage(value) +#if canImport(Synchronization) + case .mutex: + if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { + self.storage = MutexStorage(value) + } else { + preconditionFailure("DBThreadSafeLock.mutex requires a supported OS version") + } +#endif + } } - + /// Reads the value stored /// - Returns: The value stored in the container. public func read() -> T { - lock.readLock() - defer { lock.unlock() } - return value + storage.readValue() } - + public func read(_ closure: (_ value: T) throws -> Void) rethrows { - lock.readLock() - defer { lock.unlock() } - try closure(value) + try storage.withReadValue(closure) } - + public func read(_ closure: (_ value: T) throws -> U) rethrows -> U { - lock.readLock() - defer { lock.unlock() } - return try closure(value) + try storage.withReadValue(closure) } /// Executes a closure while holding an exclusive lock on the stored value. public func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { - lock.writeLock() - defer { lock.unlock() } - return try closure(&value) + try storage.withWriteValue(closure) } - + /// Replaces current value with a new one /// - Parameter newValue: The new value to be stored in the container. public func write(_ newValue: T) { - lock.writeLock() - defer { lock.unlock() } - value = newValue + storage.overwrite(with: newValue) } - + /// Returns current value in a closure with possibility to make multiple modifications of any kind inside a single lock. public func write(_ closure: (_ value: inout T) throws -> Void) rethrows { - try withLock(closure) + try storage.withWriteValue(closure) } } diff --git a/Sources/DBThreadSafe/DBThreadSafeLock.swift b/Sources/DBThreadSafe/DBThreadSafeLock.swift new file mode 100644 index 0000000..7b86f6a --- /dev/null +++ b/Sources/DBThreadSafe/DBThreadSafeLock.swift @@ -0,0 +1,11 @@ +/// Selects the synchronization backend used by `DBThreadSafeContainer`. +public enum DBThreadSafeLock: Sendable { + /// Uses the `pthread_rwlock_t` backend. Multiple readers may proceed concurrently. + case pthreadRWLock + +#if canImport(Synchronization) + /// Uses the `Synchronization.Mutex` backend. + @available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) + case mutex +#endif +} diff --git a/Sources/DBThreadSafe/Lock.swift b/Sources/DBThreadSafe/Lock.swift index ec0076f..2733ab5 100644 --- a/Sources/DBThreadSafe/Lock.swift +++ b/Sources/DBThreadSafe/Lock.swift @@ -1,27 +1,142 @@ import Foundation +#if canImport(Synchronization) +import Synchronization +#endif + +class LockStorage: @unchecked Sendable { + var lockType: DBThreadSafeLock { + fatalError("Subclasses must override lockType") + } + + func readValue() -> T { + withReadValue { $0 } + } + + func withReadValue(_ closure: (_ value: T) throws -> U) rethrows -> U { + fatalError("Subclasses must override withReadValue(_:)") + } + + func overwrite(with newValue: T) { + withWriteValue { value in + value = newValue + } + } + + func withWriteValue(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + fatalError("Subclasses must override withWriteValue(_:)") + } +} + +final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { + nonisolated(unsafe) private var value: T + private let lock = Lock() + + override var lockType: DBThreadSafeLock { + .pthreadRWLock + } + + init(_ value: T) { + self.value = value + } + + override func withReadValue(_ closure: (_ value: T) throws -> U) rethrows -> U { + lock.readLock() + defer { lock.unlock() } + return try closure(value) + } + + override func withWriteValue(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + lock.writeLock() + defer { lock.unlock() } + return try closure(&value) + } +} + +#if canImport(Synchronization) +@available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) +final class MutexStorage: LockStorage, @unchecked Sendable { + nonisolated(unsafe) private var value: T + private let mutex: Mutex + private let readDepthToken = NSObject() + + override var lockType: DBThreadSafeLock { + .mutex + } + + init(_ value: T) { + self.value = value + self.mutex = Mutex(()) + } + + override func withReadValue(_ closure: (_ value: T) throws -> U) rethrows -> U { + try withNestedReadSupport(closure) + } + + override func withWriteValue(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + try mutex.withLock { _ in + try closure(&value) + } + } + + private var readDepthKey: String { + "dbthreadsafe.mutex.readDepth.\(UInt(bitPattern: Unmanaged.passUnretained(readDepthToken).toOpaque()))" + } + + private var currentReadDepth: Int { + Thread.current.threadDictionary[readDepthKey] as? Int ?? 0 + } + + private func incrementReadDepth() { + Thread.current.threadDictionary[readDepthKey] = currentReadDepth + 1 + } + + private func decrementReadDepth() { + let newValue = currentReadDepth - 1 + + if newValue > 0 { + Thread.current.threadDictionary[readDepthKey] = newValue + } else { + Thread.current.threadDictionary.removeObject(forKey: readDepthKey) + } + } + + private func withNestedReadSupport(_ closure: (_ value: T) throws -> U) rethrows -> U { + if currentReadDepth > 0 { + return try closure(value) + } + + return try mutex.withLock { _ in + incrementReadDepth() + defer { decrementReadDepth() } + + return try closure(value) + } + } +} +#endif final class Lock: Sendable { nonisolated(unsafe) private let lock = UnsafeMutablePointer.allocate(capacity: 1) - + init() { precondition(pthread_rwlock_init(lock, nil) == 0, "Failed to initialize the lock") } - + func readLock() { pthread_rwlock_rdlock(lock) } - + func writeLock() { pthread_rwlock_wrlock(lock) } - + func unlock() { pthread_rwlock_unlock(lock) } - + deinit { precondition(pthread_rwlock_destroy(lock) == 0, "Failed to destroy the lock") - + lock.deallocate() } } diff --git a/Sources/DBThreadSafe/ThreadSafe.swift b/Sources/DBThreadSafe/ThreadSafe.swift index dad0543..2497bb1 100644 --- a/Sources/DBThreadSafe/ThreadSafe.swift +++ b/Sources/DBThreadSafe/ThreadSafe.swift @@ -8,6 +8,10 @@ public final class ThreadSafe { self.container = DBThreadSafeContainer(wrappedValue) } + public init(wrappedValue: T, lock: DBThreadSafeLock) { + self.container = DBThreadSafeContainer(wrappedValue, lock: lock) + } + public var wrappedValue: T { get { container.read() @@ -22,6 +26,7 @@ public final class ThreadSafe { } } + /// Exposes the underlying container, including `read`, `write`, `withLock`, and `lockType`. public var projectedValue: DBThreadSafeContainer { container } diff --git a/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift b/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift index 830226f..38855e3 100644 --- a/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift +++ b/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift @@ -6,6 +6,42 @@ import Testing struct DBThreadSafeContainerTests { let iterations = 100000 + @Test("Explicit pthread rwlock selection reports pthread backend") + func explicitPThreadRWLockSelection() { + let container = DBThreadSafeContainer(0, lock: .pthreadRWLock) + + #expect(container.lockType == .pthreadRWLock) + #expect(container.read() == 0) + } + +#if canImport(Synchronization) + @available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) + @Test("Explicit mutex selection reports mutex backend") + func explicitMutexSelection() { + let container = DBThreadSafeContainer(0, lock: .mutex) + + #expect(container.lockType == .mutex) + #expect(container.read() == 0) + } +#endif + + @Test("Default initializer keeps pthread backend") + func defaultInitializerKeepsPThreadBackend() { + let container = DBThreadSafeContainer(0) + + #expect(container.lockType == .pthreadRWLock) + #expect(container.read() == 0) + } + + @Test("Default backend preserves nested read access") + func defaultBackendPreservesNestedReadAccess() { + let container = DBThreadSafeContainer(0) + + container.read { _ in + #expect(container.read() == 0) + } + } + @Test("Concurrent reads return correct value") func concurrentGet() { let container = DBThreadSafeContainer(0) diff --git a/Tests/DBThreadSafeTests/ThreadSafeTests.swift b/Tests/DBThreadSafeTests/ThreadSafeTests.swift index c327643..8a7c292 100644 --- a/Tests/DBThreadSafeTests/ThreadSafeTests.swift +++ b/Tests/DBThreadSafeTests/ThreadSafeTests.swift @@ -6,6 +6,33 @@ import Testing struct ThreadSafeTests { let iterations = 100000 + @Test("Property wrapper supports explicit pthread rwlock selection") + func explicitPThreadRWLockSelection() { + @ThreadSafe(lock: .pthreadRWLock) var counter = 42 + + #expect(counter == 42) + #expect($counter.lockType == .pthreadRWLock) + } + +#if canImport(Synchronization) + @available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) + @Test("Property wrapper supports explicit mutex selection") + func explicitMutexSelection() { + @ThreadSafe(lock: .mutex) var counter = 42 + + #expect(counter == 42) + #expect($counter.lockType == .mutex) + } +#endif + + @Test("Property wrapper default keeps pthread backend") + func defaultSelectionKeepsPThreadBackend() { + @ThreadSafe var counter = 42 + + #expect(counter == 42) + #expect($counter.lockType == .pthreadRWLock) + } + @Test("Reading wrappedValue returns correct value") func readWrappedValue() { @ThreadSafe var counter = 42 From cbafa63f7eb6a53efe2ed64573ca8bf859066117 Mon Sep 17 00:00:00 2001 From: Aleksey Berezka Date: Thu, 30 Apr 2026 20:36:14 +0500 Subject: [PATCH 3/4] feat: prefer mutex backend by default Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 14 +-- .../DBThreadSafe/DBThreadSafeContainer.swift | 32 +++++-- Sources/DBThreadSafe/Lock.swift | 94 ++++++++++--------- .../DBThreadSafeContainerTests.swift | 18 +++- Tests/DBThreadSafeTests/ThreadSafeTests.swift | 12 ++- 5 files changed, 102 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index b717ca0..433091a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdodobrands%2FDBThreadSafe-ios%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/dodobrands/DBThreadSafe-ios) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdodobrands%2FDBThreadSafe-ios%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/dodobrands/DBThreadSafe-ios) -DBThreadSafeContainer is a generic class that provides thread-safe read and write access to a stored value. By default it uses a `pthread_rwlock_t` backend and can opt into Apple's `Synchronization.Mutex` on supported OS versions. +DBThreadSafeContainer is a generic class that provides thread-safe read and write access to a stored value. By default it prefers Apple's `Synchronization.Mutex` on supported OS versions and falls back to `pthread_rwlock_t` elsewhere. ## Usage @@ -17,13 +17,13 @@ To create a new instance of DBThreadSafeContainer, simply initialize it with an let container = DBThreadSafeContainer("Hello, World!") ``` -The default initializer keeps the `pthread_rwlock_t` backend for backwards-compatible read semantics: +The default initializer prefers `Synchronization.Mutex` when the current OS supports it: You can inspect the chosen backend through `lockType`: ```swift let container = DBThreadSafeContainer("Hello, World!") -let lockType = container.lockType // .pthreadRWLock +let lockType = container.lockType // .mutex on supported OS versions, otherwise .pthreadRWLock ``` ### Selecting a lock backend explicitly @@ -34,6 +34,8 @@ Use `DBThreadSafeLock` to force a specific backend: let pthreadContainer = DBThreadSafeContainer("Hello, World!", lock: .pthreadRWLock) ``` +Use this explicit opt-out when you need the old `pthread_rwlock_t` semantics with concurrent readers or same-thread nested reads. + `Synchronization.Mutex` can only be selected on supported platforms: ```swift @@ -66,7 +68,7 @@ container.withLock { value in } ``` -The existing `read` and `write` APIs remain available. +The existing `read` and `write` APIs remain available. When the mutex backend is active, both of them route through the same exclusive critical section as `withLock`. ### Reading the value @@ -135,9 +137,9 @@ try container.write { value in DBThreadSafeContainer ensures that read and write operations are thread-safe, but the exact semantics depend on the selected backend: - `pthread_rwlock_t`: multiple readers can proceed concurrently, while writes remain exclusive -- `Synchronization.Mutex`: both reads and writes are exclusive critical sections +- `Synchronization.Mutex`: `read`, `write`, and `withLock` all use the same exclusive critical section -The default initializer preserves concurrent reader behavior. If you explicitly choose `Synchronization.Mutex`, reads become exclusive critical sections just like writes. +The default initializer now prefers `Synchronization.Mutex` when available. If you need concurrent-reader behavior or same-thread nested `read` calls, select `.pthreadRWLock` explicitly. The mutex backend is not re-entrant: calling `read` again from inside `read`/`withLock` on the same container can deadlock. ## Cleanup diff --git a/Sources/DBThreadSafe/DBThreadSafeContainer.swift b/Sources/DBThreadSafe/DBThreadSafeContainer.swift index b06df9e..5ef3529 100644 --- a/Sources/DBThreadSafe/DBThreadSafeContainer.swift +++ b/Sources/DBThreadSafe/DBThreadSafeContainer.swift @@ -9,17 +9,31 @@ public final class DBThreadSafeContainer: Sendable { } public init(_ value: T) { - self.storage = PThreadRWLockStorage(value) + self.storage = Self.makeDefaultStorage(value) } public init(_ value: T, lock: DBThreadSafeLock) { + self.storage = Self.makeStorage(value, lock: lock) + } + + private static func makeDefaultStorage(_ value: T) -> LockStorage { + #if canImport(Synchronization) + if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { + return MutexStorage(value) + } + #endif + + return PThreadRWLockStorage(value) + } + + private static func makeStorage(_ value: T, lock: DBThreadSafeLock) -> LockStorage { switch lock { case .pthreadRWLock: - self.storage = PThreadRWLockStorage(value) + return PThreadRWLockStorage(value) #if canImport(Synchronization) case .mutex: if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { - self.storage = MutexStorage(value) + return MutexStorage(value) } else { preconditionFailure("DBThreadSafeLock.mutex requires a supported OS version") } @@ -30,30 +44,30 @@ public final class DBThreadSafeContainer: Sendable { /// Reads the value stored /// - Returns: The value stored in the container. public func read() -> T { - storage.readValue() + storage.read() } public func read(_ closure: (_ value: T) throws -> Void) rethrows { - try storage.withReadValue(closure) + try storage.read(closure) } public func read(_ closure: (_ value: T) throws -> U) rethrows -> U { - try storage.withReadValue(closure) + try storage.read(closure) } /// Executes a closure while holding an exclusive lock on the stored value. public func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { - try storage.withWriteValue(closure) + try storage.withLock(closure) } /// Replaces current value with a new one /// - Parameter newValue: The new value to be stored in the container. public func write(_ newValue: T) { - storage.overwrite(with: newValue) + storage.write(newValue) } /// Returns current value in a closure with possibility to make multiple modifications of any kind inside a single lock. public func write(_ closure: (_ value: inout T) throws -> Void) rethrows { - try storage.withWriteValue(closure) + try storage.write(closure) } } diff --git a/Sources/DBThreadSafe/Lock.swift b/Sources/DBThreadSafe/Lock.swift index 2733ab5..1e92ad6 100644 --- a/Sources/DBThreadSafe/Lock.swift +++ b/Sources/DBThreadSafe/Lock.swift @@ -8,22 +8,24 @@ class LockStorage: @unchecked Sendable { fatalError("Subclasses must override lockType") } - func readValue() -> T { - withReadValue { $0 } + func read() -> T { + fatalError("Subclasses must override read()") } - func withReadValue(_ closure: (_ value: T) throws -> U) rethrows -> U { - fatalError("Subclasses must override withReadValue(_:)") + func read(_ closure: (_ value: T) throws -> U) rethrows -> U { + fatalError("Subclasses must override read(_:)") } - func overwrite(with newValue: T) { - withWriteValue { value in - value = newValue - } + func write(_ newValue: T) { + fatalError("Subclasses must override write(_:)") + } + + func write(_ closure: (_ value: inout T) throws -> Void) rethrows { + fatalError("Subclasses must override write(_:)") } - func withWriteValue(_ closure: (_ value: inout T) throws -> U) rethrows -> U { - fatalError("Subclasses must override withWriteValue(_:)") + func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + fatalError("Subclasses must override withLock(_:)") } } @@ -39,13 +41,31 @@ final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { self.value = value } - override func withReadValue(_ closure: (_ value: T) throws -> U) rethrows -> U { + override func read() -> T { + lock.readLock() + defer { lock.unlock() } + return value + } + + override func read(_ closure: (_ value: T) throws -> U) rethrows -> U { lock.readLock() defer { lock.unlock() } return try closure(value) } - override func withWriteValue(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + override func write(_ newValue: T) { + lock.writeLock() + defer { lock.unlock() } + value = newValue + } + + override func write(_ closure: (_ value: inout T) throws -> Void) rethrows { + lock.writeLock() + defer { lock.unlock() } + try closure(&value) + } + + override func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { lock.writeLock() defer { lock.unlock() } return try closure(&value) @@ -57,7 +77,6 @@ final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { final class MutexStorage: LockStorage, @unchecked Sendable { nonisolated(unsafe) private var value: T private let mutex: Mutex - private let readDepthToken = NSObject() override var lockType: DBThreadSafeLock { .mutex @@ -68,48 +87,31 @@ final class MutexStorage: LockStorage, @unchecked Sendable { self.mutex = Mutex(()) } - override func withReadValue(_ closure: (_ value: T) throws -> U) rethrows -> U { - try withNestedReadSupport(closure) - } - - override func withWriteValue(_ closure: (_ value: inout T) throws -> U) rethrows -> U { - try mutex.withLock { _ in - try closure(&value) + override func read() -> T { + withLock { value in + value } } - private var readDepthKey: String { - "dbthreadsafe.mutex.readDepth.\(UInt(bitPattern: Unmanaged.passUnretained(readDepthToken).toOpaque()))" - } - - private var currentReadDepth: Int { - Thread.current.threadDictionary[readDepthKey] as? Int ?? 0 - } - - private func incrementReadDepth() { - Thread.current.threadDictionary[readDepthKey] = currentReadDepth + 1 - } - - private func decrementReadDepth() { - let newValue = currentReadDepth - 1 - - if newValue > 0 { - Thread.current.threadDictionary[readDepthKey] = newValue - } else { - Thread.current.threadDictionary.removeObject(forKey: readDepthKey) + override func read(_ closure: (_ value: T) throws -> U) rethrows -> U { + try withLock { value in + try closure(value) } } - private func withNestedReadSupport(_ closure: (_ value: T) throws -> U) rethrows -> U { - if currentReadDepth > 0 { - return try closure(value) + override func write(_ newValue: T) { + withLock { value in + value = newValue } + } - return try mutex.withLock { _ in - incrementReadDepth() - defer { decrementReadDepth() } + override func write(_ closure: (_ value: inout T) throws -> Void) rethrows { + try withLock(closure) + } - return try closure(value) + override func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + try mutex.withLock { _ in + try closure(&value) } } } diff --git a/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift b/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift index 38855e3..9a28541 100644 --- a/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift +++ b/Tests/DBThreadSafeTests/DBThreadSafeContainerTests.swift @@ -25,17 +25,25 @@ struct DBThreadSafeContainerTests { } #endif - @Test("Default initializer keeps pthread backend") - func defaultInitializerKeepsPThreadBackend() { + @Test("Default initializer prefers mutex backend when available") + func defaultInitializerPrefersMutexBackendWhenAvailable() { let container = DBThreadSafeContainer(0) + #if canImport(Synchronization) + if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { + #expect(container.lockType == .mutex) + } else { + #expect(container.lockType == .pthreadRWLock) + } + #else #expect(container.lockType == .pthreadRWLock) + #endif #expect(container.read() == 0) } - @Test("Default backend preserves nested read access") - func defaultBackendPreservesNestedReadAccess() { - let container = DBThreadSafeContainer(0) + @Test("Explicit pthread backend preserves nested read access") + func explicitPThreadBackendPreservesNestedReadAccess() { + let container = DBThreadSafeContainer(0, lock: .pthreadRWLock) container.read { _ in #expect(container.read() == 0) diff --git a/Tests/DBThreadSafeTests/ThreadSafeTests.swift b/Tests/DBThreadSafeTests/ThreadSafeTests.swift index 8a7c292..61fa5c4 100644 --- a/Tests/DBThreadSafeTests/ThreadSafeTests.swift +++ b/Tests/DBThreadSafeTests/ThreadSafeTests.swift @@ -25,12 +25,20 @@ struct ThreadSafeTests { } #endif - @Test("Property wrapper default keeps pthread backend") - func defaultSelectionKeepsPThreadBackend() { + @Test("Property wrapper default prefers mutex backend when available") + func defaultSelectionPrefersMutexBackendWhenAvailable() { @ThreadSafe var counter = 42 #expect(counter == 42) + #if canImport(Synchronization) + if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { + #expect($counter.lockType == .mutex) + } else { + #expect($counter.lockType == .pthreadRWLock) + } + #else #expect($counter.lockType == .pthreadRWLock) + #endif } @Test("Reading wrappedValue returns correct value") From b459b6bc06e1021ad27b0590314285e89855de98 Mon Sep 17 00:00:00 2001 From: Aleksey Berezka Date: Thu, 30 Apr 2026 20:59:15 +0500 Subject: [PATCH 4/4] refactor: make LockStorage a protocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DBThreadSafe/DBThreadSafeContainer.swift | 6 +- Sources/DBThreadSafe/Lock.swift | 58 ++++++++----------- .../DBThreadSafeTests/LockStorageTests.swift | 24 ++++++++ 3 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 Tests/DBThreadSafeTests/LockStorageTests.swift diff --git a/Sources/DBThreadSafe/DBThreadSafeContainer.swift b/Sources/DBThreadSafe/DBThreadSafeContainer.swift index 5ef3529..2c22d03 100644 --- a/Sources/DBThreadSafe/DBThreadSafeContainer.swift +++ b/Sources/DBThreadSafe/DBThreadSafeContainer.swift @@ -1,7 +1,7 @@ import Foundation public final class DBThreadSafeContainer: Sendable { - private let storage: LockStorage + private let storage: any LockStorage /// The concrete lock backend currently used by the container. public var lockType: DBThreadSafeLock { @@ -16,7 +16,7 @@ public final class DBThreadSafeContainer: Sendable { self.storage = Self.makeStorage(value, lock: lock) } - private static func makeDefaultStorage(_ value: T) -> LockStorage { + private static func makeDefaultStorage(_ value: T) -> any LockStorage { #if canImport(Synchronization) if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) { return MutexStorage(value) @@ -26,7 +26,7 @@ public final class DBThreadSafeContainer: Sendable { return PThreadRWLockStorage(value) } - private static func makeStorage(_ value: T, lock: DBThreadSafeLock) -> LockStorage { + private static func makeStorage(_ value: T, lock: DBThreadSafeLock) -> any LockStorage { switch lock { case .pthreadRWLock: return PThreadRWLockStorage(value) diff --git a/Sources/DBThreadSafe/Lock.swift b/Sources/DBThreadSafe/Lock.swift index 1e92ad6..5dd98ad 100644 --- a/Sources/DBThreadSafe/Lock.swift +++ b/Sources/DBThreadSafe/Lock.swift @@ -3,37 +3,25 @@ import Foundation import Synchronization #endif -class LockStorage: @unchecked Sendable { - var lockType: DBThreadSafeLock { - fatalError("Subclasses must override lockType") - } - - func read() -> T { - fatalError("Subclasses must override read()") - } - - func read(_ closure: (_ value: T) throws -> U) rethrows -> U { - fatalError("Subclasses must override read(_:)") - } - - func write(_ newValue: T) { - fatalError("Subclasses must override write(_:)") - } +protocol LockStorage: AnyObject, Sendable { + associatedtype Value - func write(_ closure: (_ value: inout T) throws -> Void) rethrows { - fatalError("Subclasses must override write(_:)") + var lockType: DBThreadSafeLock { + get } - func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { - fatalError("Subclasses must override withLock(_:)") - } + func read() -> Value + func read(_ closure: (_ value: Value) throws -> U) rethrows -> U + func write(_ newValue: Value) + func write(_ closure: (_ value: inout Value) throws -> Void) rethrows + func withLock(_ closure: (_ value: inout Value) throws -> U) rethrows -> U } -final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { +final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { nonisolated(unsafe) private var value: T private let lock = Lock() - override var lockType: DBThreadSafeLock { + var lockType: DBThreadSafeLock { .pthreadRWLock } @@ -41,31 +29,31 @@ final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { self.value = value } - override func read() -> T { + func read() -> T { lock.readLock() defer { lock.unlock() } return value } - override func read(_ closure: (_ value: T) throws -> U) rethrows -> U { + func read(_ closure: (_ value: T) throws -> U) rethrows -> U { lock.readLock() defer { lock.unlock() } return try closure(value) } - override func write(_ newValue: T) { + func write(_ newValue: T) { lock.writeLock() defer { lock.unlock() } value = newValue } - override func write(_ closure: (_ value: inout T) throws -> Void) rethrows { + func write(_ closure: (_ value: inout T) throws -> Void) rethrows { lock.writeLock() defer { lock.unlock() } try closure(&value) } - override func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { lock.writeLock() defer { lock.unlock() } return try closure(&value) @@ -74,11 +62,11 @@ final class PThreadRWLockStorage: LockStorage, @unchecked Sendable { #if canImport(Synchronization) @available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) -final class MutexStorage: LockStorage, @unchecked Sendable { +final class MutexStorage: LockStorage, @unchecked Sendable { nonisolated(unsafe) private var value: T private let mutex: Mutex - override var lockType: DBThreadSafeLock { + var lockType: DBThreadSafeLock { .mutex } @@ -87,29 +75,29 @@ final class MutexStorage: LockStorage, @unchecked Sendable { self.mutex = Mutex(()) } - override func read() -> T { + func read() -> T { withLock { value in value } } - override func read(_ closure: (_ value: T) throws -> U) rethrows -> U { + func read(_ closure: (_ value: T) throws -> U) rethrows -> U { try withLock { value in try closure(value) } } - override func write(_ newValue: T) { + func write(_ newValue: T) { withLock { value in value = newValue } } - override func write(_ closure: (_ value: inout T) throws -> Void) rethrows { + func write(_ closure: (_ value: inout T) throws -> Void) rethrows { try withLock(closure) } - override func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { + func withLock(_ closure: (_ value: inout T) throws -> U) rethrows -> U { try mutex.withLock { _ in try closure(&value) } diff --git a/Tests/DBThreadSafeTests/LockStorageTests.swift b/Tests/DBThreadSafeTests/LockStorageTests.swift new file mode 100644 index 0000000..4f0591b --- /dev/null +++ b/Tests/DBThreadSafeTests/LockStorageTests.swift @@ -0,0 +1,24 @@ +@testable import DBThreadSafe +import Testing + +@Suite("LockStorage Protocol Tests") +struct LockStorageTests { + @Test("LockStorage supports existential storage for pthread backend") + func existentialPThreadStorage() { + let storage: any LockStorage = PThreadRWLockStorage(42) + + #expect(storage.lockType == .pthreadRWLock) + #expect(storage.read() == 42) + } + +#if canImport(Synchronization) + @available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) + @Test("LockStorage supports existential storage for mutex backend") + func existentialMutexStorage() { + let storage: any LockStorage = MutexStorage(42) + + #expect(storage.lockType == .mutex) + #expect(storage.read() == 42) + } +#endif +}