Skip to content
Merged
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
64 changes: 61 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,59 @@ To create a new instance of DBThreadSafeContainer, simply initialize it with an
let container = DBThreadSafeContainer("Hello, World!")
```

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 // .mutex on supported OS versions, otherwise .pthreadRWLock
```

### Selecting a lock backend explicitly

Use `DBThreadSafeLock` to force a specific backend:

```swift
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
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`:

```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. When the mutex backend is active, both of them route through the same exclusive critical section as `withLock`.

### Reading the value

To read the value stored in the container, use the `read()` method:
Expand Down Expand Up @@ -81,11 +134,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`: `read`, `write`, and `withLock` all use the same exclusive critical section

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

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
76 changes: 52 additions & 24 deletions Sources/DBThreadSafe/DBThreadSafeContainer.swift
Original file line number Diff line number Diff line change
@@ -1,45 +1,73 @@
import Foundation

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

private let storage: any LockStorage<T>

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

public init(_ value: T) {
self.value = 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) -> any LockStorage<T> {
#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) -> any LockStorage<T> {
switch lock {
case .pthreadRWLock:
return PThreadRWLockStorage(value)
#if canImport(Synchronization)
case .mutex:
if #available(iOS 18, macCatalyst 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) {
return 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.read()
}

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

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


/// Executes a closure while holding an exclusive lock on the stored value.
public func withLock<U>(_ closure: (_ value: inout T) throws -> U) rethrows -> U {
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) {
lock.writeLock()
defer { lock.unlock() }
value = 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 {
lock.writeLock()
defer { lock.unlock() }
try closure(&value)
try storage.write(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
}
117 changes: 111 additions & 6 deletions Sources/DBThreadSafe/Lock.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,132 @@
import Foundation
#if canImport(Synchronization)
import Synchronization
#endif

protocol LockStorage<Value>: AnyObject, Sendable {
associatedtype Value

var lockType: DBThreadSafeLock {
get
}

func read() -> Value
func read<U>(_ closure: (_ value: Value) throws -> U) rethrows -> U
func write(_ newValue: Value)
func write(_ closure: (_ value: inout Value) throws -> Void) rethrows
func withLock<U>(_ closure: (_ value: inout Value) throws -> U) rethrows -> U
}

final class PThreadRWLockStorage<T>: LockStorage, @unchecked Sendable {
nonisolated(unsafe) private var value: T
private let lock = Lock()

var lockType: DBThreadSafeLock {
.pthreadRWLock
}

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

func read() -> T {
lock.readLock()
defer { lock.unlock() }
return value
}

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

func write(_ newValue: T) {
lock.writeLock()
defer { lock.unlock() }
value = newValue
}

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

func withLock<U>(_ 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<T>: LockStorage, @unchecked Sendable {
nonisolated(unsafe) private var value: T
private let mutex: Mutex<Void>

var lockType: DBThreadSafeLock {
.mutex
}

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

func read() -> T {
withLock { value in
value
}
}

func read<U>(_ closure: (_ value: T) throws -> U) rethrows -> U {
try withLock { value in
try closure(value)
}
}

func write(_ newValue: T) {
withLock { value in
value = newValue
}
}

func write(_ closure: (_ value: inout T) throws -> Void) rethrows {
try withLock(closure)
}

func withLock<U>(_ closure: (_ value: inout T) throws -> U) rethrows -> U {
try mutex.withLock { _ in
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()
}
}
5 changes: 5 additions & 0 deletions Sources/DBThreadSafe/ThreadSafe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ public final class ThreadSafe<T> {
self.container = DBThreadSafeContainer(wrappedValue)
}

public init(wrappedValue: T, lock: DBThreadSafeLock) {
self.container = DBThreadSafeContainer(wrappedValue, lock: lock)
}

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

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