niksativa/threading
Threading is a framework that provides type-safe abstractions for thread synchronization and concurrent programming in Swift. It offers a set of tools for working with locks, mutexes, and atomic operations, making concurrent code safer and easier to write.
Overview
Threading provides several key components:
- Queue: A type-safe wrapper around Grand Central Dispatch queues
- Mutexing: A protocol for thread-safe value access
- Locking: A protocol for basic synchronization primitives
- AtomicValue: A property wrapper for thread-safe properties
Topics
Queue Management
- `
Queue`: A type-safe wrapper for GCD queues; provides convenient access to system queues, custom queue creation, and safe main-thread access. - `
Queueable`: A protocol for queue-like types that exposes a common interface for queue operations. - `
DelayedQueue`: A queue with built-in delay support, ideal for throttling, rate limiting, and cancelable delayed operations.
Synchronization
- `
Mutexing: A protocol for synchronized access to values; supports blocking and non-blocking execution with error propagation. Supports@dynamicCallableand@dynamicMemberLookup` for convenient syntax. - `
Locking: A protocol for lock implementations, supporting core locking behaviors, including recursive locks. Supports@dynamicCallable` for functional-style syntax. - `
AnyMutex: A type-erased wrapper around anyMutexing` implementation. - `
AnyLock: A type-erased wrapper around anyLocking` implementation.
Mutex Implementations
- `
SyncMutex`: A native mutex using the system synchronization framework (macOS 15.0+, iOS 18.0+); supports recursive locking and integrates with debugging tools. - `
OSAllocatedUnfairMutex`: A high-performance mutex using OS-allocated unfair locks (macOS 13.0+, iOS 16.0+). - `
QueueBarrier`: A mutex that uses dispatch queue barriers for synchronization; useful for queue-based concurrency patterns. - `
LockedValue`: A generic mutex-backed value container that allows custom locking strategies.
Property Wrappers
- `
AtomicValue`: A property wrapper that synchronizes access to a property using a configurable mutex.
Concurrency Utilities
- `
USendable`: A lightweight wrapper for non-Sendable types, useful when bridging legacy types into concurrency contexts. Requires manual thread safety.
Usage
Working with Queues
Perform synchronous work on the main thread safely:
Queue.main.sync {
// Your task on main thread
}Perform asynchronous work:
Queue.main.async {
// Your task on main thread
}Create custom queues with explicit parameters:
let customQueue = Queue.custom(
label: "com.example.queue",
qos: .utility,
attributes: .serial
)
customQueue.async {
// Your work here
}Use DelayedQueue for delayed or conditional execution:
// Execute after a delay
let delayed = DelayedQueue.n.asyncAfter(deadline: .now() + 2, queue: .main)
delayed.fire {
print("Executed after 2 seconds")
}
// Synchronous execution
let sync = DelayedQueue.n.sync(.main)
sync.fire {
print("Executed synchronously")
}
// Asynchronous execution
let async = DelayedQueue.n.async(.background)
async.fire {
print("Executed asynchronously")
}Thread-Safe Value Access
Use mutexes to protect shared values:
let counter = LockedValue(initialValue: 0)
// Safely modify the value
counter.sync { value in
value += 1
}
// Get the current value
let currentValue = counter.sync { value in
return value
}
// Using dynamic callable syntax (functional style)
counter { $0 += 1 }
let doubled = counter { $0 * 2 }Use trySync for non-blocking access:
if let value = counter.trySync({ $0 }) {
print("Current value: \(value)")
}AtomicValue Properties
Use the @AtomicValue property wrapper for thread-safe properties:
@AtomicValue var counter = 0
// Using sync method
$counter.sync { $0 += 1 }
// Using dynamic callable syntax (functional style)
$counter { $0 = 10 }
// Direct property access (thread-safe)
counter = 5
let value = counterYou can also specify a custom mutex type:
@AtomicValue(mutexing: SyncMutex.self) var counter = 0
@AtomicValue(lock: .osAllocatedUnfair()) var highPerformance = 0Concurrency Workarounds
When working with @MainActor-isolated APIs:
Queue.isolatedMain.sync {
// Access UI elements safely
view.backgroundColor = .red
}Working with Non-Sendable Types
The USendable type provides a way to work with non-Sendable types in concurrent contexts. It's particularly useful when you need to:
- Access UIKit/AppKit objects from concurrent contexts
- Work with legacy code that hasn't been updated for Swift concurrency
- Bridge between synchronous and asynchronous code
[!WARNING] Important:
USendabledoesn't make the wrapped value thread-safe. It only marks the type as@unchecked Sendable. You must ensure thread safety through other means, such as:
- Accessing the value only on the main thread
- Using proper synchronization mechanisms
- Following the value's thread-safety requirements
// Wrap a UIKit view
let unsafe = USendable(ImageView())
// Access it safely on the main thread
Queue.main.async {
let view = unsafe.value
view.backgroundColor = .red
}
// Or use it in a Task with proper synchronization
Task { @MainActor in
let view = unsafe.value
view.backgroundColor = .blue
}Advanced Features
Dynamic Callable Syntax
Both Locking and Mutexing protocols support @dynamicCallable, allowing functional-style syntax:
// Using Locking protocol
let lock: Locking = AnyLock.default
lock {
// Critical section
performWork()
}
// Using Mutexing protocol
let counter = LockedValue(initialValue: 0)
counter { $0 += 1 }
let value = counter { $0 }
// Using AtomicValue
@AtomicValue var count = 0
$count.sync { $0 += 1 }
// Or using dynamic callable syntax (works for any type)
$count { $0 += 1 }Dynamic Member Lookup
Mutexing and AtomicValue support @dynamicMemberLookup for convenient property access:
@AtomicValue var user = User(name: "Alice", age: 30)
// Thread-safe property access
let name = user.name // Automatically synchronized
user.age = 31 // Thread-safe mutationBest Practices
- Keep Critical Sections Short
```swift // Good counter.sync { value in value += 1 }
// Avoid counter.sync { value in // Long-running operations block other threads performExpensiveOperation() } ```
- Choose Appropriate Mutex Type
- Use SyncMutex for general-purpose synchronization (macOS 15.0+, iOS 18.0+) - Use OSAllocatedUnfairMutex for high-performance scenarios (macOS 13.0+, iOS 16.0+) - Use QueueBarrier when working with GCD queues - Use LockedValue when you need custom lock behavior - Use AnyLock.default (recursive pthread) as a safe default
- Handle Errors Properly
``swift do { try mutex.sync { value in try performRiskyOperation(value) } } catch { // Handle error appropriately } ``
- Use Try-Lock for Non-Blocking Operations
``swift // Non-blocking access if let result = lock.trySync({ computeValue() }) { // Lock acquired, work completed } else { // Lock busy, handle accordingly } ``
Requirements
- iOS 13.0+ / macOS 11.0+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+
- Swift 5.5+
- Xcode 13.0+
Note: Some features require newer platform versions:
SyncMutex: macOS 15.0+, iOS 18.0+, tvOS 18.0+, watchOS 11.0+, visionOS 2.0+OSAllocatedUnfairMutex/OSAllocatedUnfairLock: macOS 13.0+, iOS 16.0+, tvOS 16.0+, watchOS 9.0+
Installation
Swift Package Manager
Add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/NikSativa/Threading.git", from: "1.0.0")
]Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Package Metadata
Repository: niksativa/threading
Default branch: main
README: README.md