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:
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:
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:
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:
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:
Task.detached {
let data = try await fetchData(from: "https://example.com")
print("Detached task fetched \(data.count) bytes")
}
Example: Task Group:
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:
@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:
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:
@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
orcheckCancellation
. - 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
@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)"
}
}
}