Skip to content

Concurrency

Swift 5.5 introduced structured concurrency with async/await, actors, and tasks, replacing older patterns like Grand Central Dispatch (GCD) and callbacks. This system ensures thread safety, reduces race conditions, and improves asynchronous code readability.

Async/Await

Mark asynchronous functions with async and call them with await.

Example: Basic Async Function:

swift
func fetchData(from url: String) async throws -> Data {
    // Simulate network request
    try await Task.sleep(nanoseconds: 1_000_000_000)
    return Data()
}

Task {
    do {
        let data = try await fetchData(from: "https://example.com")
        print("Fetched \(data.count) bytes")
    } catch {
        print("Error: \(error)")
    }
}

Example: Chaining Async Calls:

swift
func processImage(_ data: Data) async -> String {
    await Task.sleep(nanoseconds: 500_000_000)
    return "Processed image"
}

Task {
    do {
        let data = try await fetchData(from: "https://example.com/image")
        let result = await processImage(data)
        print(result)
    } catch {
        print(error)
    }
}

Actors

Actors are reference types that protect mutable state from concurrent access, ensuring thread safety.

Example: Basic Actor:

swift
actor Counter {
    private var count = 0
    
    func increment() -> Int {
        count += 1
        return count
    }
    
    func getCount() -> Int {
        return count
    }
}

let counter = Counter()
Task {
    let value = await counter.increment()
    print("Count: \(value)")
}

Task {
    let value = await counter.increment()
    print("Count: \(value)")
}

Example: Actor with Multiple Methods:

swift
actor BankAccount {
    private var balance: Double = 0.0
    
    func deposit(_ amount: Double) throws {
        guard amount > 0 else { throw BankError.invalidAmount }
        balance += amount
    }
    
    func withdraw(_ amount: Double) throws -> Double {
        guard amount <= balance else { throw BankError.insufficientFunds }
        balance -= amount
        return balance
    }
}

enum BankError: Error {
    case invalidAmount
    case insufficientFunds
}

let account = BankAccount()
Task {
    do {
        try await account.deposit(100.0)
        let newBalance = try await account.withdraw(50.0)
        print("Remaining balance: \(newBalance)")
    } catch {
        print(error)
    }
}

Tasks and Task Groups

Task runs asynchronous code; task groups manage parallel tasks.

Example: Detached Task:

swift
Task.detached {
    let data = try await fetchData(from: "https://example.com")
    print("Detached task fetched \(data.count) bytes")
}

Example: Task Group:

swift
func fetchAll(_ urls: [String]) async throws -> [Data] {
    try await withThrowingTaskGroup(of: Data.self) { group in
        for url in urls {
            group.addTask {
                return try await fetchData(from: url)
            }
        }
        var results: [Data] = []
        for try await data in group {
            results.append(data)
        }
        return results
    }
}

Task {
    do {
        let datas = try await fetchAll(["url1", "url2", "url3"])
        print("Fetched \(datas.count) items")
    } catch {
        print(error)
    }
}

MainActor

Ensures code runs on the main thread, critical for UI updates.

Example:

swift
@MainActor
func updateUI(with text: String) {
    // Update UIKit or SwiftUI views
    print("Updating UI with \(text)")
}

Task {
    let data = try await fetchData(from: "https://example.com")
    await updateUI(with: "Data fetched")
}

Structured Concurrency

Swift ensures tasks are hierarchically managed, preventing orphaned tasks.

Example: Cancelling Tasks:

swift
func longRunningTask() async throws {
    try Task.checkCancellation()
    try await Task.sleep(nanoseconds: 5_000_000_000)
    print("Task completed")
}

let task = Task {
    try await longRunningTask()
}
task.cancel() // Cancels task

Global Actor

Custom global actors manage shared resources.

Example:

swift
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
func saveRecord(_ record: String) async {
    print("Saving \(record) to database")
}

Task {
    await saveRecord("User123")
}

Best Practices

  • Use Async/Await: Prefer over callbacks or GCD.
  • Actors for State: Protect mutable state with actors.
  • Task Groups: Use for parallel work with clear results.
  • MainActor for UI: Ensure UI updates are main-thread-safe.
  • Handle Cancellation: Check Task.isCancelled or checkCancellation.
  • Avoid Blocking: Never use synchronous sleeps or locks in async code.

Troubleshooting

  • Deadlocks: Ensure actors don’t block each other.
  • Cancellation Ignored: Implement cancellation checks.
  • Main Thread Issues: Use @MainActor for UI.
  • Task Leaks: Ensure tasks complete or are cancelled.
  • Performance: Profile with Instruments for async bottlenecks.

Example: Comprehensive Concurrency Usage

swift
@MainActor
class AppViewModel {
    var status: String = "Idle"
    
    func fetchAndUpdate() async throws {
        status = "Fetching..."
        let data = try await fetchData(from: "https://api.example.com")
        let results = try await processData(data)
        status = "Updated with \(results.count) items"
    }
}

actor DataProcessor {
    func process(_ data: Data) async throws -> [String] {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return ["Item1", "Item2"]
    }
}

func fetchData(from url: String) async throws -> Data {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    return Data()
}

let viewModel = AppViewModel()
let processor = DataProcessor()

Task {
    do {
        try await viewModel.fetchAndUpdate()
        print(await viewModel.status)
    } catch {
        await MainActor.run {
            viewModel.status = "Error: \(error)"
        }
    }
}

Released under the MIT License.