Skip to content
This repository was archived by the owner on Sep 16, 2026. It is now read-only.
minaclePublic archive

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

swift-locks

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.

Requirements and installation

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.

API map

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

Exclusive access: Lock to Mutex

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.

Read/write access: ReadWriteLock to RWLock

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.

Nonblocking acquisition

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.

Protected properties: replacing @LockManaged

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.

Asynchronous critical sections

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.

Recursive locking: restructure the critical section

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.

Migration checklist

  • 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; remove import Locks and 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.