Swift & iOS

Review GCD scheduling, actor basics, ARC ownership, retain cycles, and concurrency fundamentals.

~7 min reading
Suggest content

Concurrency and Memory

🟪 Syntax · 🟦 Types/APIs · 🟩 Correct pattern · 🟥 Bug · 🟨 Gotcha · 🧠 Explanation
This sheet covers foundational GCD, ARC, and actor topics. For current Swift 6 isolation and task patterns, see 11-Swift-Concurrency-6.4.md.

Serial, concurrent, main, and global queues

  • A serial queue executes one submitted block at a time.
  • A concurrent queue may execute multiple blocks simultaneously.
  • The main queue is serial and runs work on the main thread.
  • Global queues are system-provided concurrent queues with different QoS classes.
  • A custom serial queue is not the same thing as the main queue and is not a permanent dedicated thread.

Queues manage work; the system chooses underlying threads. A global concurrent queue generally begins submitted work in FIFO order, but completion order is not guaranteed.

SWIFT
let serialQueue = DispatchQueue(label: "com.example.cache")
​
serialQueue.async {
    // Serialized with other work submitted to this queue.
}

sync versus async

  • sync blocks the caller until the submitted work finishes.
  • async returns immediately and lets the queue schedule the work.
  • sync does not automatically make code safe; it can cause deadlock or block a valuable thread.

Never synchronously dispatch to the same serial queue currently executing the caller:

SWIFT
DispatchQueue.main.sync {
    // Deadlock if already on the main thread.
}

Common GCD deadlocks

Same serial queue

Code running on a serial queue calls queue.sync. The queue cannot start the inner block until the outer block finishes, while the outer block waits for the inner block.

Main queue wait

A background task waits synchronously for the main queue while the main thread waits for that background task.

Circular resource wait

Two queues or locks each own a resource and wait for the other resource.

Prevention:

  • Prefer structured concurrency and async APIs.
  • Avoid synchronous cross-queue call chains.
  • Keep locks small and acquire them in a consistent order.
  • Do not block the main thread.
  • Encapsulate shared mutable state behind an actor, serial queue, or lock instead of mixing mechanisms.

Concurrent reads with barrier writes

SWIFT
final class ThreadSafeStore<Key: Hashable, Value> {
    private var storage: [Key: Value] = [:]
    private let queue = DispatchQueue(
        label: "com.example.store",
        attributes: .concurrent
    )
​
    func value(for key: Key) -> Value? {
        queue.sync { storage[key] }
    }
​
    func set(_ value: Value, for key: Key) {
        queue.async(flags: .barrier) { [weak self] in
            self?.storage[key] = value
        }
    }
}

Reads can run concurrently. A barrier write waits for earlier work, runs exclusively, and prevents later work from beginning until it completes. Barriers provide this behavior on a custom concurrent queue, not as a general guarantee on a global queue.

Be clear about write completion semantics. An asynchronous write may not be visible immediately to the caller; an actor or async store API often communicates this more honestly.

Actors

An actor protects its isolated mutable state so only one task accesses that state at a time.

SWIFT
actor TokenStore {
    private var token: String?
​
    func update(_ token: String) {
        self.token = token
    }
​
    func currentToken() -> String? {
        token
    }
}

From outside the actor:

SWIFT
let token = await tokenStore.currentToken()

Actor isolation protects internal state; it does not automatically redesign the public API or make every related operation safe. Values that leave the actor should be safe to transfer, and multi-step operations may still need to remain inside one actor method to preserve invariants.

Actors are reentrant: when an actor method suspends at await, other work may enter the actor. Recheck assumptions after suspension.

Sendable

Sendable communicates that a value can safely cross concurrency isolation boundaries.

Good natural candidates:

  • Immutable value types whose stored properties are Sendable
  • Carefully synchronized reference types
  • Actor references

Codable and Sendable solve different problems:

  • Codable: serialization and deserialization
  • Sendable: concurrency safety when transferring values

A response model does not need Sendable merely because it is decoded. Add it when the model crosses task or actor boundaries and its contents satisfy the contract.

Async/await and priorities

  • Task supports priorities, but priority is a scheduling hint, not a strict order guarantee.
  • Prefer inherited priority in structured child tasks.
  • Avoid creating detached tasks unless work truly has no parent context.
  • Cancellation is cooperative: check Task.isCancelled or call Task.checkCancellation() during longer work.
  • UI-facing view models are often isolated to @MainActor.

Operation cancellation

Calling cancel() on an Operation normally marks it as cancelled. It does not forcibly terminate a running thread. Custom operations must check isCancelled, stop starting new work, and finish cleanly. Underlying URL tasks or other cancellable work should also be cancelled.

ARC

ARC tracks strong references to class instances. When the strong reference count reaches zero, the instance is deinitialized and its memory can be reclaimed.

ARC does not detect or automatically break retain cycles.

Typical cycle:

TEXT
owner -> closure/subscription -> owner

Use:

  • weak when the reference may become nil
  • unowned only when the referenced object must outlive the holder
  • explicit cancellation or teardown for long-lived callbacks

A lazy property does not inherently create a retain cycle. A lazy closure that captures self strongly and is retained by self can create one.

Interview comparison: GCD versus actors

GCDActors
Queue-based scheduling primitiveLanguage-level isolation model
Safety depends on disciplined queue usageCompiler helps enforce isolation
Works with synchronous and callback-era APIsFits structured concurrency and async/await
Barriers and sync access require careful semanticsActor methods expose asynchronous boundary clearly

Use actors for new shared mutable state when deployment constraints allow. GCD remains important for existing APIs, low-level scheduling, and legacy code.