Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .xcode-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16
26
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 prefers Apple's `Synchronization.Mutex` on supported OS versions and falls back to `pthread_rwlock_t` elsewhere.

## Usage

Expand All @@ -17,6 +17,40 @@ To create a new instance of DBThreadSafeContainer, simply initialize it with an
let container = DBThreadSafeContainer("Hello, World!")
```

The default initializer automatically selects the concrete lock backend:

- `Synchronization.Mutex` on iOS 18+, macOS 15+, macCatalyst 18+, tvOS 18+, watchOS 11+, visionOS 2+
- `pthread_rwlock_t` everywhere else

You can inspect the chosen backend through `lockType`:

```swift
let container = DBThreadSafeContainer("Hello, World!")
let lockType = container.lockType
```

### 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
```

### Reading the value

To read the value stored in the container, use the `read()` method:
Expand Down Expand Up @@ -81,11 +115,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

Because the default initializer now prefers `Mutex` on supported OS versions, the same source code may use concurrent reads on older systems and exclusive reads on newer ones.

## 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

Expand Down
68 changes: 44 additions & 24 deletions Sources/DBThreadSafe/DBThreadSafeContainer.swift
Original file line number Diff line number Diff line change
@@ -1,45 +1,65 @@
import Foundation

public final class DBThreadSafeContainer<T>: Sendable {
nonisolated(unsafe) private var value: T
private let lock = Lock()

private let storage: LockStorage<T>

/// The concrete lock backend currently used by the container.
public var lockType: DBThreadSafeLock {
storage.lockType
}

/// Creates a container that prefers `Synchronization.Mutex` when the current OS supports it
/// and otherwise falls back to the `pthread_rwlock_t` backend.
public init(_ value: T) {
self.value = value
#if canImport(Synchronization)
if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) {
self.storage = MutexStorage(value)
} else {
self.storage = PThreadRWLockStorage(value)
}
#else
self.storage = PThreadRWLockStorage(value)
#endif
}

/// Creates a container using the explicitly requested lock backend.
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<U>(_ closure: (_ value: T) throws -> U) rethrows -> U {
lock.readLock()
defer { lock.unlock() }
return try closure(value)
try storage.withReadValue(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 {
lock.writeLock()
defer { lock.unlock() }
try closure(&value)
try storage.withWriteValue(closure)
}
}
11 changes: 11 additions & 0 deletions Sources/DBThreadSafe/DBThreadSafeLock.swift
Original file line number Diff line number Diff line change
@@ -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
}
163 changes: 157 additions & 6 deletions Sources/DBThreadSafe/Lock.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,178 @@
import Foundation
#if canImport(Synchronization)
import Synchronization
#endif

class LockStorage<T>: @unchecked Sendable {
var lockType: DBThreadSafeLock {
fatalError("Subclasses must override lockType")
}

func readValue() -> T {
fatalError("Subclasses must override readValue()")
}

func withReadValue(_ closure: (_ value: T) throws -> Void) rethrows {
fatalError("Subclasses must override withReadValue(_:)")
}

func withReadValue<U>(_ closure: (_ value: T) throws -> U) rethrows -> U {
fatalError("Subclasses must override withReadValue(_:)")
}

func overwrite(with newValue: T) {
fatalError("Subclasses must override overwrite(with:)")
}

func withWriteValue(_ closure: (_ value: inout T) throws -> Void) rethrows {
fatalError("Subclasses must override withWriteValue(_:)")
}
}

final class PThreadRWLockStorage<T>: LockStorage<T>, @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 readValue() -> T {
lock.readLock()
defer { lock.unlock() }
return value
}

override func withReadValue(_ closure: (_ value: T) throws -> Void) rethrows {
lock.readLock()
defer { lock.unlock() }
try closure(value)
}

override func withReadValue<U>(_ closure: (_ value: T) throws -> U) rethrows -> U {
lock.readLock()
defer { lock.unlock() }
return try closure(value)
}

override func overwrite(with newValue: T) {
lock.writeLock()
defer { lock.unlock() }
value = newValue
}

override func withWriteValue(_ closure: (_ value: inout T) throws -> Void) rethrows {
lock.writeLock()
defer { lock.unlock() }
try closure(&value)
}
}

#if canImport(Synchronization)
@available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *)
final class MutexStorage<T>: LockStorage<T>, @unchecked Sendable {
nonisolated(unsafe) private var value: T
private let mutex: Mutex<Void>
private let readDepthToken = NSObject()

override var lockType: DBThreadSafeLock {
.mutex
}

init(_ value: T) {
self.value = value
self.mutex = Mutex(())
}

override func readValue() -> T {
withNestedReadSupport { value in
value
}
}

override func withReadValue(_ closure: (_ value: T) throws -> Void) rethrows {
try withNestedReadSupport(closure)
}

override func withReadValue<U>(_ closure: (_ value: T) throws -> U) rethrows -> U {
try withNestedReadSupport(closure)
}

override func overwrite(with newValue: T) {
mutex.withLock { _ in
value = newValue
}
}

override func withWriteValue(_ closure: (_ value: inout T) throws -> Void) rethrows {
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<U>(_ 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<pthread_rwlock_t>.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()
}
}
8 changes: 8 additions & 0 deletions Sources/DBThreadSafe/ThreadSafe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import Foundation
public final class ThreadSafe<T> {
private let container: DBThreadSafeContainer<T>

/// Creates a thread-safe wrapper that prefers `Synchronization.Mutex` when available and
/// otherwise falls back to the `pthread_rwlock_t` backend.
public init(wrappedValue: T) {
self.container = DBThreadSafeContainer(wrappedValue)
}

/// Creates a thread-safe wrapper using the explicitly requested lock backend.
public init(wrappedValue: T, lock: DBThreadSafeLock) {
self.container = DBThreadSafeContainer(wrappedValue, lock: lock)
}

public var wrappedValue: T {
get {
container.read()
Expand All @@ -22,6 +29,7 @@ public final class ThreadSafe<T> {
}
}

/// Exposes the underlying container, including `read`, `write`, and `lockType`.
public var projectedValue: DBThreadSafeContainer<T> {
container
}
Expand Down
Loading
Loading