Concurrency and Multithreading in Swift: Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Concurrency and Multithreading in Swift: Advanced Level

\n\n

Modern applications for iOS and macOS require high performance and responsive interfaces. Advanced work with concurrency in Swift goes far beyond simple use of Grand Central Dispatch (GCD). In this article, we will explore key Swift 5.5+ tools for managing asynchronous tasks: async/await, Actors, TaskGroup, and the Sendable protocol. You will learn how to avoid data races, properly structure multithreaded code, and optimize performance.

\n\n

1. Basics of async/await and Structured Concurrency

\n\n

The keywords async and await are the foundation of modern concurrency in Swift. They allow you to write asynchronous code that looks synchronous, greatly simplifying reading and debugging.

\n\n

Declaring an asynchronous function:

\n
func fetchUserData() async throws -> User {\n    let url = URL(string: "https://api.example.com/user")!\n    let (data, _) = try await URLSession.shared.data(from: url)\n    let decoder = JSONDecoder()\n    return try decoder.decode(User.self, from: data)\n}
\n\n

Calling an asynchronous function with error handling:

\n
Task {\n    do {\n        let user = try await fetchUserData()\n        print("User name: \\(user.name)")\n    } catch {\n        print("Failed to fetch user: \\(error)")\n    }\n}
\n\n

Important points:

\n
    \n
  • Structured Concurrency: Each asynchronous task has a clear lifetime, scoped to the parent Task.
  • \n
  • Task Cancellation: Automatic cancellation of child tasks when the parent Task is cancelled.
  • \n
  • Priorities: Use Task(priority: .userInitiated) for tasks important to the user.
  • \n
\n\n

2. Actors: Protection from Data Races

\n\n

Before Swift 5.5, developers often used serialized GCD queues to protect shared state. Actors provide built-in isolation: access to an actor's state is only possible via await, guaranteeing thread safety at the compiler level.

\n\n

Example Actor for a bank account:

\n
actor BankAccount {\n    private var balance: Double = 0.0\n\n    func deposit(amount: Double) {\n        balance += amount\n    }\n\n    func withdraw(amount: Double) throws {\n        guard balance >= amount else {\n            throw BankError.insufficientFunds\n        }\n        balance -= amount\n    }\n\n    func getBalance() -> Double {\n        return balance\n    }\n}\n\n// Usage\nlet account = BankAccount()\nTask {\n    await account.deposit(amount: 100.0)\n    let currentBalance = await account.getBalance()\n    print("Balance: \\(currentBalance)")\n}
\n\n

Advantages of Actors:

\n
    \n
  • Compiler checking: Attempting to access an actor's property without await will cause a compile-time error.
  • \n
  • Reentrancy: An actor can suspend execution of the current method (e.g., during await inside the actor) and handle another call, preventing deadlocks.
  • \n
  • Performance: Actors use optimized synchronization mechanisms, often faster than manual locks.
  • \n
\n\n

3. TaskGroup: Parallel Data Processing

\n\n

When you need to perform several independent asynchronous operations and wait for them all to complete, TaskGroup is the ideal tool. It automatically manages the number of concurrent tasks and collects results.

\n\n

Example of downloading multiple images in parallel:

\n
func downloadImages(urls: [URL])                

Blogs

Book Recommendations