# Synchronization Primitives

When an actor is the wrong shape for some shared mutable state, these types
protect it instead. Each one is `Sendable` and safe to use from any thread.

## Mutex

`Mutex<Value>` lives in the `Synchronization` module and needs iOS 18.0 or
later. It guarantees mutual exclusion: a thread that asks for a lock someone
else holds waits until it is released. Documentation:
https://developer.apple.com/documentation/synchronization/mutex

```swift
import Synchronization
import UIKit

final class AvatarCache: Sendable {
    private let images = Mutex<[String: UIImage]>([:])

    func image(for handle: String) -> UIImage? {
        images.withLock { $0[handle] }
    }

    func store(_ image: UIImage, for handle: String) {
        images.withLock { $0[handle] = image }
    }

    func purge() {
        images.withLock { $0.removeAll() }
    }
}
```

`withLockIfAvailable` tries once and returns `nil` instead of waiting when the
lock is taken:

```swift
let pendingFlushes = Mutex<Int>(0)

if let count = pendingFlushes.withLockIfAvailable({ $0 }) {
    metrics.report(pendingFlushes: count)
} else {
    // contended: skip this sample
}
```

Properties worth remembering:

- It is generic over the protected value and stores that value itself, so it
  is always clear what the lock guards.
- It is `Sendable`, so it can sit in a `Sendable` class, in global state, or
  (rarely) next to an actor.
- It is not recursive. Locking it again on the thread that holds it is
  undefined behavior.
- It is synchronous. The lock is held for the entire `withLock` closure, so
  never `await` inside it; suspending while holding a lock can deadlock or
  starve the thread pool.

## OSAllocatedUnfairLock

`OSAllocatedUnfairLock<State>` comes from the `os` module and needs iOS 16.0.
It wraps `os_unfair_lock` in a safe API. Because it allocates the lock on the
heap, it avoids the problem that makes a raw `os_unfair_lock` unsound in Swift:
Swift gives no stable address for a stored struct. Documentation:
https://developer.apple.com/documentation/os/osallocatedunfairlock

Guarding state:

```swift
import os

enum UploadPhase: Sendable {
    case waiting
    case sending(progress: Double)
    case done(receipt: String)
    case failed(any Error)
}

final class UploadTracker: Sendable {
    private let phase = OSAllocatedUnfairLock(initialState: UploadPhase.waiting)

    func update(_ newPhase: UploadPhase) {
        phase.withLock { $0 = newPhase }
    }

    var current: UploadPhase {
        phase.withLock { $0 }
    }
}
```

Guarding a critical section with no state inside the lock:

```swift
let journalLock = OSAllocatedUnfairLock()

func appendToJournal(_ line: String) {
    journalLock.withLock {
        writeLine(line, toFileAt: journalURL)
    }
}
```

`lock()` and `unlock()` also exist but are discouraged. If you use them, unlock
on the same thread that locked, never span an `await`, and pair them with
`defer`:

```swift
journalLock.lock()
defer { journalLock.unlock() }
writeLine("checkpoint", toFileAt: journalURL)
```

## Mutex or OSAllocatedUnfairLock

| | `Mutex` | `OSAllocatedUnfairLock` |
|---|---|---|
| Minimum OS | iOS 18 | iOS 16 |
| Module | `Synchronization` | `os` |
| State | Always stores a generic `Value` | Optional, via `initialState:` |
| Non-blocking attempt | `withLockIfAvailable` | `withLockIfAvailable` |
| Ownership checks | None | `precondition(.owner)`, `precondition(.notOwner)` |
| Manual `lock()` / `unlock()` | No | Yes |

- Targeting iOS 18 or later: use `Mutex`.
- Targeting iOS 16 or 17: use `OSAllocatedUnfairLock`.
- Supporting iOS 16 through the current release: build the shared abstraction
  on `OSAllocatedUnfairLock`, or branch with `#available(iOS 18, *)` and use
  `Mutex` where it exists.
- Want to assert lock ownership while debugging: `OSAllocatedUnfairLock`.

## Atomic

`Atomic<Value>` is in `Synchronization` and needs iOS 18.0. It performs
lock-free operations on types conforming to `AtomicRepresentable` and suits
counters, flags and compare-and-exchange where a lock would be overkill.
Documentation: https://developer.apple.com/documentation/synchronization/atomic

```swift
import Synchronization

final class ActiveStreams: Sendable {
    private let count = Atomic<Int>(0)

    func opened() { count.wrappingAdd(1, ordering: .relaxed) }
    func closed() { count.wrappingSubtract(1, ordering: .relaxed) }
    var value: Int { count.load(ordering: .relaxed) }
}

final class Teardown: Sendable {
    private let finished = Atomic<Bool>(false)

    func runOnce() {
        let (won, _) = finished.compareExchange(
            expected: false,
            desired: true,
            ordering: .acquiringAndReleasing
        )
        guard won else { return }
        closeSockets()
    }
}
```

### Memory ordering

Every atomic call names an ordering.

| Ordering | Use |
|---|---|
| `.relaxed` | Counters and statistics; no ordering relative to other memory |
| `.acquiring` | A load that must see everything written before the matching release |
| `.releasing` | A store whose earlier writes must be visible to the matching acquire |
| `.acquiringAndReleasing` | Compare-and-exchange and other read-modify-write operations |
| `.sequentiallyConsistent` | Strongest and slowest; rarely required, use only with a proven need |

Rule of thumb: `.relaxed` for plain counters, `.acquiringAndReleasing` for
compare-and-exchange.

### Atomic or Mutex

- **Atomic:** a single scalar such as `Int`, `Bool` or `UInt64`, updated one
  field at a time: counters, flags. Lock-free and very fast.
- **Mutex:** compound state such as dictionaries or structs with several
  fields, and any sequence of steps that must happen as one unit.

## Locks or actors

### Choose an actor when

- the state is reached from async code and the cost of hopping is acceptable;
- callers can suspend. From outside, an actor's API is `async`, which rules it
  out for synchronous C callbacks, real-time audio hooks and similar call sites;
- you want the compiler to enforce the isolation boundary;
- there is no special constraint: actors are the default for shared mutable
  state in most Swift code;
- the state is rich and has many operations, since isolation covers all of
  them at once.

```swift
actor ArtworkLoader {
    private var loaded: [URL: UIImage] = [:]

    func artwork(at url: URL) async throws -> UIImage? {
        if let image = loaded[url] { return image }
        let (data, _) = try await URLSession.shared.data(from: url)
        let image = UIImage(data: data)
        loaded[url] = image
        return image
    }
}
```

### Choose a lock when

- callers must stay synchronous; calling an actor from sync code requires a
  `Task` and makes the operation asynchronous;
- the path is hot: taking an uncontended lock costs nanoseconds while an actor
  hop involves scheduling, so tight loops can be far faster with a lock;
- C or Objective-C callbacks, delegates or APIs that cannot become async touch
  the state;
- the state is one counter or flag, where `Atomic<Int>` or `Atomic<Bool>` is
  both cheaper and simpler than an actor.

```swift
final class EventTally: Sendable {
    private let counts = Mutex<[String: Int]>([:])

    func record(_ name: String) {
        counts.withLock { $0[name, default: 0] += 1 }
    }

    func snapshot() -> [String: Int] {
        counts.withLock { $0 }
    }
}
```

## Decision guide

| Situation | Use |
|---|---|
| Every access can be async | An actor |
| Some access must be synchronous | `Mutex`, or `OSAllocatedUnfairLock` below iOS 18 |
| One counter or flag | `Atomic<Value>` |
| Nanosecond-sensitive path | `Mutex` or `Atomic` |
| C or Objective-C callbacks | `Mutex` or `OSAllocatedUnfairLock` |

## Anti-patterns

**A lock inside an actor.** The actor already serializes access; the lock adds
a second layer and a way to deadlock.

```swift
// Wrong
actor PrefetchQueueWrong {
    private let pending = Mutex<[URL]>([])
}

// Right
actor PrefetchQueue {
    private var pending: [URL] = []
    func enqueue(_ url: URL) { pending.append(url) }
}
```

**Old primitives by reflex.** `DispatchSemaphore` and `NSLock` should not be the
first tool in modern Swift. `NSLock` is `Sendable`, so it is legal, but `Mutex`
(iOS 18) and `OSAllocatedUnfairLock` (iOS 16) tie the lock to the state it
protects and make ownership clearer. Keep `NSLock` for compatibility or where an
existing API expects it.

**A lock held across `await`.** The thread stays blocked while the task is
suspended, which can exhaust or deadlock the cooperative pool.

```swift
// Wrong: suspends while holding the lock
// latest.withLock { $0 = await feed.fetch() }

// Right: await first, lock only to publish the result
let fresh = await feed.fetch()
latest.withLock { $0 = fresh }
```
