Swift Concurrency


Introduction to Concurrency in Swift

Concurrency in Swift allows executing multiple tasks simultaneously without blocking the main thread. It improves the performance and responsiveness of applications, especially in networking, file handling, and UI updates.

With Swift 5.5+, Apple introduced modern concurrency features such as:

  • async/await – Simplifies asynchronous programming.
  • Task & TaskGroup – Manages concurrent operations.
  • Actors – Ensures thread safety for shared resources.

This guide explains Swift concurrency with real-world examples.

1. Understanding async and await in Swift

Swift introduced async functions to perform non-blocking operations.

Declaring an async Function

func fetchData() async -> String {
     return "Data fetched successfully"
}

Task {
     let result = await fetchData()
     print(result)
}

Output

Data fetched successfully

async marks a function as asynchronous.
await pauses execution until the function returns a result.

2. Performing Network Requests with async and await

import Foundation

func fetchUsers() async throws -> [String] {
     let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
     let (data, _) = try await URLSession.shared.data(from: url)
     let users = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
     return users?.compactMap { $0["name"] as? String } ?? []
}

Task {
     do {
         let users = try await fetchUsers()
         print(users)
     } catch {
         print("Failed to fetch users:", error)
     }
}

Output

["Leanne Graham", "Ervin Howell", ...]

try await ensures error handling in async functions.
URLSession.shared.data(from:) works natively with async/await.

3. Running Multiple Tasks Concurrently with async let

Swift allows running multiple asynchronous operations in parallel.

func fetchPosts() async -> String {
     return "Posts fetched"
}

func fetchComments() async -> String {
     return "Comments fetched"
}

Task {
     async let posts = fetchPosts()
     async let comments = fetchComments()

     let result = await "\(posts) & \(comments)"
     print(result)
}

Output

Posts fetched & Comments fetched

async let starts tasks simultaneously.
await ensures all operations complete before execution continues.

4. Using Task to Manage Background Work

Task allows launching concurrent tasks from synchronous code.

Task {
     let data = await fetchData()
     print(data)
}

Runs in the background without blocking the main thread.

Running a Task with Priority

Task(priority: .high) {
     print("High priority task running")
}

Priorities: .high, .medium, .low, .background.

5. Task Cancellation in Swift

Tasks can be canceled to avoid unnecessary processing.

func downloadFile() async {
     for i in 1...10 {
         if Task.isCancelled {
             print("Download canceled")
             return
         }
         print("Downloading... \(i * 10)% completed")
         try? await Task.sleep(nanoseconds: 1_000_000_000) // Simulating delay
     }
     print("Download completed")
}

let task = Task {
     await downloadFile()
}

// Cancel task after 3 seconds
Task {
     try? await Task.sleep(nanoseconds: 3_000_000_000)
     task.cancel()
}

Output

Downloading... 10% completed
Downloading... 20% completed
Downloading... 30% completed
Download canceled

Task.isCancelled ensures efficient resource management.
Tasks stop execution immediately when canceled.

6. Structured Concurrency with TaskGroup

A TaskGroup allows running multiple related tasks concurrently and waiting for them to complete.

func fetchData(for id: Int) async -> String {
     return "Data for ID \(id)"
}

func fetchMultipleData() async {
     await withTaskGroup(of: String.self) { group in
         for i in 1...3 {
             group.addTask {
                 await fetchData(for: i)
             }
         }

         for await result in group {
             print(result)
         }
     }
}

Task {
     await fetchMultipleData()
}

Output

Data for ID 1
Data for ID 2
Data for ID 3

Efficient parallel execution of multiple tasks.
withTaskGroup ensures all tasks complete before proceeding.

7. Using Actors for Thread Safety

Actor ensures data consistency by preventing race conditions in concurrent operations.

actor BankAccount {
     private var balance: Int = 0

     func deposit(amount: Int) {
         balance += amount
     }

     func getBalance() -> Int {
         return balance
     }
}

let account = BankAccount()

Task {
     await account.deposit(amount: 100)
     print("Balance: \(await account.getBalance())")
}

Output

Balance: 100

actor ensures safe access to shared data.
await is required when accessing actor properties/methods.

8. Async Sequences in Swift

Swift allows processing streaming data asynchronously using AsyncSequence.

struct Counter: AsyncSequence, AsyncIteratorProtocol {
     var count = 0

     mutating func next() async -> Int? {
         count += 1
         return count <= 5 ? count : nil
     }

     func makeAsyncIterator() -> Counter {
         return self
     }
}

Task {
     for await num in Counter() {
         print(num)
     }
}

Output

1
2
3
4
5

AsyncSequence provides efficient handling of streaming data.

Best Practices for Swift Concurrency

  • Use async/await for cleaner, more readable asynchronous code.
  • Use async let for parallel execution of independent tasks.
  • Use TaskGroup to manage structured concurrency.
  • Always check Task.isCancelled for graceful cancellation.
  • Use actors to ensure thread safety in shared data.

Conclusion

Swift concurrency provides powerful tools for handling asynchronous programming with async/await, Task, TaskGroup, and Actors. These features ensure safe, efficient, and scalable concurrent programming.