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.
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
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
// 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
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.
// 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
}
}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 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).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.
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."