Swift Concurrency: Parallel Tasks
Part 1: Basics - Synchronous code, GCD, and async/await fundamentals.
Part 2: Parallel Execution - Running independent tasks concurrently.
Part 3: Errors & Cancellation - Propagation and task cancellation.
Part 4: Actors - Protecting shared state with actor isolation.
- XC Project - Download the project with all examples used in this series
In the previous chapter, each resource (image, invites count, and friends list) was loaded one after the other. While this sequential approach is simple and readable, it can be inefficient when resources are independent. In this chapter, we’ll see how Swift Concurrency lets us fetch multiple resources in parallel, reducing total execution time while keeping the code clean and easy to understand.
This example updates multiple related resources for a single person, but fetches them in parallel instead of sequentially. Each tab shows the same operation implemented with GCD and Swift Concurrency, allowing you to compare the syntax and structure of parallel execution in both approaches.
func updatePersonInfoParallel(person: Person) { let dispatchGroup = DispatchGroup() var personDetailsTemp = PersonDetails() dispatchGroup.enter() person.loadImage { personImage in personDetailsTemp.personImage = personImage dispatchGroup.leave() } dispatchGroup.enter() person.loadInvitesCount { invitesCount in personDetailsTemp.invitesCount = invitesCount dispatchGroup.leave() } dispatchGroup.enter() person.loadFriends { friends in personDetailsTemp.friends = friends dispatchGroup.leave() } dispatchGroup.notify(queue: .main) { [weak self] in self? .personDetails = personDetailsTemp } }
func updatePersonInfoParallel(person: Person) async { async let personImage = try? person.loadImage() async let invitesCount = try? person.loadInvitesCount() async let friends = try? person.loadFriends() let (image, count, friendsList) = await (personImage, invitesCount, friends) personDetails = .init( personImage: image, invitesCount: count, friends: friendsList ) }
The same operation is performed in parallel using Grand Central Dispatch with a DispatchGroup and using Swift Concurrency. While the performance of both approaches is comparable, Swift Concurrency offers much clearer, linear syntax, eliminating the boilerplate and callback management required by GCD. This example demonstrates how async/await can make parallel code easier to read and maintain, without sacrificing efficiency.
This example retrieves an authenticated user, refreshing the token if it has expired before continuing the request. The same control flow is implemented using GCD and Swift Concurrency to highlight how each approach handles branching logic and asynchronous dependencies.
func getAuthenticatedUser(token: Token, completion: (Result<Person, Error>) -> Void) { if !token.isValid() { TokenManager.shared.renewToken { result in switch result { case .success(let newToken): // Call same function again, with new token getAuthenticatedUser(token: newToken, completion: completion) case .failure(let tokenError): // Token refresh failed, call closure with error completion(.failure(tokenError)) } } // Return from this execution because we entered a closure return } AuthManager.shared.getAuthenticatedUser(token: token) { result in switch result { case .success(let user): // Successfully retrieved user completion(.success(user)) case .failure(let userError): // Error retrieving user completion(.failure(userError)) } } }
func getAuthenticatedUser(token: Token) async throws -> Person { var token = token do { if !token.isValid() { token = try await TokenManager.shared.renewToken() } return try await AuthManager.shared.getAuthenticatedUser(token: token) } }
This example shows the same operation implemented in two ways: using Grand Central Dispatch with nested callbacks, and using Swift Concurrency with async/await.
The GCD version requires managing multiple closures and has to include explanatory comments to help follow the flow, making it harder to read and understand. In contrast, the Swift Concurrency version expresses the same logic in linear, easy-to-follow syntax, eliminating the callback nesting and boilerplate while keeping the behavior identical.
This example fetches a collection of images for multiple people at the same time. Each implementation runs the work in parallel, allowing you to compare how GCD and Swift Concurrency handle multiple concurrent tasks and result aggregation.
func getAllPhotos(completion: @escaping ([UUID: UIImage?]) -> Void) { let dispatchGroup = DispatchGroup() var results: [UUID: UIImage?] = [:] for person in people { dispatchGroup.enter() DispatchQueue.global().async { person.loadImage() { image in results[person.id] = image dispatchGroup.leave() } } } dispatchGroup.notify(queue: .main) { completion(results) } }
func getAllPhotos() async -> [UUID: UIImage?] { await withTaskGroup(of: (UUID, UIImage?).self) { group in var results: [UUID: UIImage?] = [:] for person in people { group.addTask { await (person.id, try? person.loadImage()) } } for await (id, image) in group { results[id] = image } return results } }
This example fetches multiple images in parallel. The GCD version uses a DispatchGroup with nested closures, which requires careful handling of shared state and boilerplate. Swift Concurrency accomplishes the same work using a TaskGroup, allowing each task to run concurrently with linear, readable syntax.
Support This Content
If you found this helpful, you can buy me a coffee here.
Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…
Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…