Deprecated: use SynchronizationKit for new code.
This guide helps existing swift-locks users migrate from Locks toSynchronizationKit. It targets SynchronizationKit 1.1.0. This is an API migration: SynchronizationKit's locks own their protected values, rather than just sitting beside them.
SynchronizationKit 1.1.0 requires Swift 6.3 or later. Its minimum Apple platform versions are macOS 12, iOS 15, tvOS 15, watchOS 8, and visionOS 1. Check your application's deployment targets before replacing the dependency; swift-locks uses a Swift 5.5 package manifest and does not declare those platform minimums.
In your application's Package.swift, replace the swift-locks dependency with:
.package(
url: "https://github.com/sinoru/swift-synchronization-kit.git",
from: "1.1.0"
),Replace the Locks product dependency in each consuming target with:
.product(name: "SynchronizationKit", package: "swift-synchronization-kit"),Replace import Locks with import SynchronizationKit. All primitives used below are enabled by the package's default traits. The dependency requirement above permits compatible later 1.x releases; the examples here use the 1.1.0 API.
| Locks | SynchronizationKit migration |
|---|---|
Lock |
Mutex<Value> for synchronous exclusive access |
ReadWriteLock |
RWLock<Value> for synchronous shared reads and exclusive writes |
RecursiveLock |
No direct equivalent; restructure nested acquisition |
@LockManaged |
Store the value in Mutex<Value> or RWLock<Value> |
withLock { ... } |
withLock { value in ... } |
withLock(to: .read) { ... } |
withReadLock { value in ... } |
withLock(to: .write) { ... } |
withWriteLock { value in ... } |
tryLock() |
withLockIfAvailable { value in ... } |
tryLock(to: .read) / tryLock(to: .write) |
withReadLockIfAvailable / withWriteLockIfAvailable |
lock() / unlock() and read/write variants |
Move the whole critical section into a locking closure |
Asynchronous withLock overloads |
AsyncMutex<Value> or AsyncRWLock<Value> and try await |
Before:
import Locks
final class Counter {
private let lock = Lock()
private var count = 0
func increment() -> Int {
lock.withLock {
count += 1
return count
}
}
}After:
import SynchronizationKit
final class Counter: Sendable {
private let count = Mutex(0)
func increment() -> Int {
count.withLock { value in
value += 1
return value
}
}
}The closure receives mutable access to the stored value. The lock is released when the closure returns or throws. Convert manual lock() / unlock() pairs into the same scoped form, including every operation previously between them. Do not use underscored unsafe locking APIs as the migration path.
Before:
import Locks
final class Cache {
private let lock = ReadWriteLock()
private var entries: [String: Int] = [:]
func value(for key: String) -> Int? {
lock.withLock(to: .read) { entries[key] }
}
func store(_ value: Int, for key: String) {
lock.withLock(to: .write) { entries[key] = value }
}
}After:
import SynchronizationKit
final class Cache: Sendable {
private let entries = RWLock<[String: Int]>([:])
func value(for key: String) -> Int? {
entries.withReadLock { $0[key] }
}
func store(_ value: Int, for key: String) {
entries.withWriteLock { $0[key] = value }
}
}Read closures borrow the value; write closures receive mutable access. A read closure must not mutate an object reachable through a stored reference. Use exclusive access for mutations. Sharing RWLock<Value> or AsyncRWLock<Value> across concurrency boundaries requires Value: Sendable.
RWLock is writer-preferring. Do not rely on the old lock's scheduling order, nest acquisitions of the same lock (including read inside read), or try to upgrade a held read lock to a write lock. If a decision and its mutation must be atomic, perform both inside one write closure.
Instead of branching on tryLock() and later calling unlock(), put the work inside an IfAvailable closure:
import SynchronizationKit
let count = Mutex(0)
let updated: Int? = count.withLockIfAvailable { value in
value += 1
return value
}
if let updated {
print("Updated to \(updated)")
} else {
print("Busy; no update performed")
}The result is the closure's return value wrapped in an optional. Failed acquisition returns nil without running the closure. A closure returning Void produces Void?; one returning an optional produces a nested optional. withReadLockIfAvailable and withWriteLockIfAvailable follow the same result convention for read and write access.
There is no property-wrapper replacement. Move the stored value into a lock and expose operations that access it through closures.
Before:
import Locks
final class Settings {
@LockManaged var attempts = 0
func recordAttempt() {
attempts += 1
}
}The old wrapper locks its getter and setter separately; that does not make attempts += 1 one atomic read-modify-write operation. After migration, keep the entire update in one critical section:
import SynchronizationKit
final class Settings: Sendable {
private let state = Mutex(0)
var attempts: Int {
state.withLock { $0 }
}
func recordAttempt() {
state.withLock { $0 += 1 }
}
}Replace uses of $attempts too: the old projected value bypasses locking. There is no corresponding unprotected property to expose. For an optional initial value, initialize explicitly, for example Mutex<Int?>(nil). Use RWLock when you need concurrent read access rather than choosing a lock solely from whether the value is a class or a struct.
If several properties previously shared one lock, collect them into a single state struct held by one Mutex or RWLock. Creating a separate lock for each property would lose the shared critical section that protects their invariants.
The old asynchronous withLock overloads hold a thread-based lock across the closure, including suspension points. Do not translate them to a synchronous Mutex or RWLock held across await. Use an asynchronous lock when the critical section itself must span a suspension.
For example, replace await lock.withLock { await ... } with a value-owning AsyncMutex and a throwing asynchronous call:
import SynchronizationKit
final class AsyncCounter: Sendable {
private let count = AsyncMutex(0)
func incrementAfterYield() async throws -> Int {
try await count.withLock { value in
await Task.yield()
value += 1
return value
}
}
}For old asynchronous read/write closures, use the corresponding methods on AsyncRWLock:
import SynchronizationKit
final class AsyncCache: Sendable {
private let entries = AsyncRWLock<[String: Int]>([:])
func value(for key: String) async throws -> Int? {
try await entries.withReadLock { $0[key] }
}
func store(_ value: Int, for key: String) async throws {
try await entries.withWriteLock { $0[key] = value }
}
}These calls can throw CancellationError when cancelled while waiting, even if their closures do not throw. Propagate the error with async throws, as above, or handle it at the call site. Acquisition of a free lock is not a cancellation check: an already-cancelled task may still acquire it. Cancellation after acquisition does not forcibly end the closure; the lock is released when the closure returns or throws. Releasing a lock does not roll back earlier mutations if the closure throws.
Keep these sections short. Waiting tasks suspend, but still depend on the holder finishing. The asynchronous locks are not recursive either. An actor can be an alternative if your design permits other calls to interleave at suspension points; it is not a direct replacement for a lock held across await.
SynchronizationKit 1.1.0 has no direct replacement for RecursiveLock. Simply changing its type to Mutex would make nested acquisition invalid.
Before, one method might acquire the recursive lock and call another method that acquires it again:
import Locks
final class ResettableCounter {
private let lock = RecursiveLock()
private var count = 0
func increment() {
lock.withLock { count += 1 }
}
func resetAndIncrement() {
lock.withLock {
count = 0
increment()
}
}
}After, acquire once at the public boundary and pass the protected value to a helper that does not acquire the lock:
import SynchronizationKit
final class ResettableCounter: Sendable {
private let count = Mutex(0)
private static func incrementUnlocked(_ value: inout Int) {
value += 1
}
func increment() {
count.withLock { Self.incrementUnlocked(&$0) }
}
func resetAndIncrement() {
count.withLock { value in
value = 0
Self.incrementUnlocked(&value)
}
}
}Audit callbacks and indirect calls as well as obvious nested closures. If your API fundamentally requires recursive acquisition, it needs redesign before it can use these primitives.
- Store each protected value inside its lock, declared with
let. These locks are noncopyable structs, unlike the old lock classes. To share one instance between owners, keep it in a shared reference-type container rather than attempting to copy it. - Preserve groups of data protected by one lock, and keep compound operations inside one closure. Do not leak mutable reference state outside that scope.
- Replace every manual acquisition,
tryLock, property-wrapper projection, and asynchronous overload; removeimport Locksand the old dependency once all consuming targets have migrated. - Check for nested acquisition, read-to-write upgrades, and synchronous locks held across suspension points.
- Build and test on your supported platforms. Exercise concurrent updates, unsuccessful nonblocking acquisition, thrown closures, and cancellation of waiting tasks in the paths your application uses.
See the SynchronizationKit README for package configuration and platform details, and the API documentation for the current reference. The hosted API documentation may describe a newer release than this guide.