thecodex.expert · The Codex Family of Knowledge
Swift

Concurrency and ARC

Swift actors make data races a compile error. ARC makes memory leaks deterministic — and avoidable with weak references.

Swift 5.5+ concurrency Actor isolation Last verified:
Canonical Definition

Swift's structured concurrency uses async/await with Task and TaskGroup; actor types serialise access to their mutable state eliminating data races at compile time; @MainActor ensures UI updates happen on the main thread; ARC provides deterministic deallocation without garbage collection pauses; weak/unowned references break retain cycles.

The central idea

Grand Central Dispatch (GCD) was powerful but error-prone — data races were runtime crashes, not compile errors. Swift 5.5 replaced it with structured concurrency and actors. Now the compiler tells you when you're accessing shared state from the wrong context. And ARC tells you exactly when memory is freed — no GC pauses, no uncertainty.

async/await and Tasks

Swiftconcurrency.swift
import Foundation

// async function: can be suspended at await points
func fetchUser(id: Int) async throws -> String {
    try await Task.sleep(nanoseconds: 100_000_000)   // non-blocking sleep
    return "User\(id)"
}

// Sequential async calls
func loadProfile() async throws -> String {
    let user = try await fetchUser(id: 1)   // waits here
    let posts = try await fetchPosts(for: user)   // waits here
    return "\(user): \(posts)"
}

// Concurrent async calls with async let
func loadFeed() async throws {
    async let user = fetchUser(id: 1)   // starts immediately
    async let posts = fetchPosts(for: "User1")   // starts immediately, in parallel
    // Both running concurrently — await both at once
    let (u, p) = try await (user, posts)
    print("\(u): \(p)")
}

// Task: explicit async unit of work
func startBackgroundWork() {
    Task {
        let result = try? await fetchUser(id: 42)
        print(result ?? "failed")
    }
}

func fetchPosts(for user: String) async throws -> [String] {
    return ["Post1", "Post2"]
}

Actors: compile-time data race prevention

Swiftactors.swift
// actor: reference type where state access is serialised
actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    var currentBalance: Double { balance }
}

// @MainActor: must run on main thread — for UI updates
@MainActor
class ViewModel {
    var title: String = "Loading..."   // always accessed on main thread

    func loadData() async {
        let data = await fetchData()   // can suspend off main thread
        title = data   // back on main thread — safe
    }

    func fetchData() async -> String {
        return "Loaded!"
    }
}

// Accessing actor state requires await
let account = BankAccount()
Task {
    await account.deposit(100)
    let balance = await account.currentBalance   // await required — async access
    print("Balance: \(balance)")
    // account.balance   // COMPILE ERROR: actor-isolated — must await
}

ARC: Automatic Reference Counting

Swiftarc.swift
class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    // weak: optional reference that becomes nil when Person is deallocated
    // Breaks the retain cycle — Apartment doesn't hold Person alive
    weak var tenant: Person?
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4B")

alice?.apartment = apt
apt?.tenant = alice   // weak reference — no retain cycle

alice = nil   // prints "Alice is being deinitialized" — ARC frees immediately
apt = nil     // prints "Apartment 4B is being deinitialized"

// Closure retain cycle — common mistake:
class ViewController {
    var onDismiss: (() -> Void)?

    func setup() {
        // [weak self] prevents self from being retained by the closure
        onDismiss = { [weak self] in
            guard let self = self else { return }
            self.dismiss()
        }
    }
    func dismiss() { print("dismissed") }
}

Sendable: compile-time thread safety

The Sendable protocol marks types that are safe to pass across concurrency boundaries (actor hops, Task handoffs). Value types conforming to Sendable are always safe (they're copied). Classes can be Sendable if they are immutable or internally synchronised. The Swift compiler enforces Sendable in Swift 6 strict concurrency mode — passing a non-Sendable type across an actor boundary is a compile error. This is the mechanism that makes Swift 6's data race safety guarantee comprehensive.

Swiftsendable.swift
// Sendable struct: value type — safe to send across tasks
struct Message: Sendable {
    let id: Int
    let text: String
}

// Actor receives Sendable types safely
actor MessageQueue {
    private var messages: [Message] = []

    func enqueue(_ message: Message) {   // Message: Sendable — safe to cross actor boundary
        messages.append(message)
    }
}

// TaskGroup: structured concurrency for parallel work
func processItems(_ items: [Int]) async -> [String] {
    await withTaskGroup(of: String.self) { group in
        for item in items {
            group.addTask {
                return "Processed \(item)"
            }
        }
        var results: [String] = []
        for await result in group {
            results.append(result)
        }
        return results
    }
}
Commonly confused
ARC is not garbage collection. GC periodically scans memory and frees unreachable objects — there are GC pauses. ARC inserts retain and release calls at compile time; when a reference count reaches zero, the object is deallocated immediately at that exact point in execution. ARC is deterministic — deinit runs at a predictable time. GC is non-deterministic — finalizers may run much later. ARC's cost: atomic retain/release operations; GC's cost: pause latency.
weak vs unowned: when each is safe. weak var ref: T? — becomes nil when T is deallocated; always optional. Use when the referenced object may be deallocated before you access it (e.g. delegate pattern). unowned let ref: T — not optional; crashes if accessed after deallocation. Use only when you're certain the referenced object lives at least as long as the referencer (e.g. a child object referencing its parent).
Actors don't prevent all concurrency bugs. Actors serialise access to their own mutable state. But if actor A calls actor B which calls back actor A (re-entrant call), the state can change between the first await and the resumption — a form of TOCTOU (Time-Of-Check-To-Time-Of-Use) race. Actors are not mutexes; they are more like message queues. Design actor methods to complete atomically or handle the possibility that state changed at each await point.

The Swift cooperative thread pool

Swift's structured concurrency runs on a cooperative thread pool — a fixed-size pool of threads (default: number of CPU cores). Tasks are scheduled onto available threads; when a task hits an await, it suspends and the thread is freed for other tasks. This is similar to Go's M:N scheduler. Unlike GCD (which could create hundreds of threads leading to thread explosion), the cooperative pool stays bounded. Each continuation (the work after an await) is a closure stored on the heap. @MainActor is backed by the main run loop — all main actor continuations are dispatched via DispatchQueue.main under the hood, ensuring safe UI updates.

Swift 6 strict concurrency and the Sendable enforcement

Swift 6 (released 2024) enabled strict concurrency checking by default — previously it was opt-in via -strict-concurrency=complete. In Swift 6, passing a non-Sendable value across actor boundaries or task boundaries is a compile error. This required significant changes to existing Swift 5 codebases — particularly in Apple's own frameworks (UIKit/AppKit use non-Sendable types extensively). The migration path: annotate types with @unchecked Sendable for internal synchronisation, use sending parameters (Swift 6.0 SE-0430) for owned handoffs, or redesign APIs around actors and value types. The goal is a world where the Swift compiler guarantees no data races in code that compiles without warnings.

Specification reference

Apple Inc. The Swift Programming Language. docs.swift.org/swift-book/. Concurrency chapter. Swift Evolution: SE-0296 (async/await), SE-0302 (actors), SE-0306 (@MainActor), SE-0430 (sending parameters). Apple WWDC 2021: "Meet async/await in Swift." Apple WWDC 2022: "Eliminate data races using Swift Concurrency."

Sources

1
Apple Inc. The Swift Programming Language — Concurrency. docs.swift.org/swift-book/.
2
Swift Evolution. SE-0296 (async/await), SE-0302 (actors), SE-0430 (sending). github.com/apple/swift-evolution.
3
Apple WWDC 2021. "Meet async/await in Swift." developer.apple.com/videos.
4
Apple WWDC 2022. "Eliminate data races using Swift Concurrency." developer.apple.com/videos.
5
Apple Inc. The Swift Programming Language — Automatic Reference Counting. docs.swift.org/swift-book/.
Source confidence: High Last verified: Primary source: The Swift Programming Language — docs.swift.org/swift-book/