Concurrency and Multithreading in Swift: Advanced Level
Concurrency is one of the most complex and important topics in iOS/macOS development. At an advanced level, Swift offers powerful tools: from the classic Grand Central Dispatch (GCD) to the modern async/await model and the strict Actors system. In this article, we will break down how to efficiently manage threads, avoid data races, and write thread-safe code.
1. Grand Central Dispatch (GCD): Working with Queues and Groups
GCD remains the foundation of multithreading in Swift. However, at an advanced level, it's important to understand not only the difference between main and global(), but also how to manage task dependencies.
Creating Custom Queues
let serialQueue = DispatchQueue(label: "com.pythonlib.serial")let concurrentQueue = DispatchQueue(label: "com.pythonlib.concurrent", attributes: .concurrent)
serialQueue.async { print("Task 1 in serial queue")}concurrentQueue.async { print("Task 2 in concurrent queue")}DispatchGroup: Synchronizing Multiple Tasks
let group = DispatchGroup()let queue = DispatchQueue.global()
queue.async(group: group) { sleep(1) print("Loading data 1")}queue.async(group: group) { sleep(2) print("Loading data 2")}
group.notify(queue: DispatchQueue.main) { print("All loads complete, updating UI")}Use DispatchGroup when you need to wait for the completion of several parallel operations (e.g., loading images or parsing JSON).
2. Modern async/await Model and Task
Starting with Swift 5.5, the language gained native support for asynchrony through the async and await keywords. This simplifies code readability and eliminates the "pyramid of doom".
Declaring and Calling an Async Function
func fetchUserData() async -> String { // Simulating a network request try? await Task.sleep(nanoseconds: 2_000_000_000) return "User data loaded"}
Task { let result = await fetchUserData() print(result) // Prints "User data loaded"}Parallel Calls with async let
func loadImage() async -> Data { return Data() }func loadMetadata() async -> String { return "meta" }
Task { async let image = loadImage() async let metadata = loadMetadata() let result = try await (image, metadata) print("Image: \\(result.0), metadata: \\(result.1)")}async let allows you to launch multiple async tasks in parallel and wait for all of them. This is more efficient than sequential calls.
3. Actors: Thread Safety at the Language Level
Actors are a new mechanism for protecting against data races. They guarantee that access to their state will be synchronized automatically.
Defining and Using an Actor
actor BankAccount { private var balance: Double = 0.0 func deposit(amount: Double) { balance += amount print("Balance after deposit: \\(balance)") } func withdraw(amount: Double) -> Bool { guard balance >= amount else { return false } balance -= amount return true } func getBalance() -> Double { balance }}
Task { let account = BankAccount() await account.deposit(amount: 100) let success = await account.withdraw(amount: 50) print("Withdrawal successful: \\(success)")}All actor methods are isolated by default — they can only be called via await. This prevents accidental data modification from multiple threads.
Nonisolated Methods
actor Logger { nonisolated func log(message: String) { print("Log: \\(message)")\