Skip to main content

Command Palette

Search for a command to run...

Parallelism in Swift Concurrency with async let

Swift's concurrency model provides a powerful and elegant solution to Parallelism: the async let binding.

Updated
3 min read
Parallelism in Swift Concurrency with async let

The Bottleneck of Sequential await

In Swift's structured concurrency, the await keyword is used to pause the execution of the current task until the asynchronous function it calls returns a result. While this is essential for managing dependencies, using multiple sequential await calls for tasks that do not depend on each other forces the program to wait for each operation to complete before starting the next.

Consider a scenario where an application needs to fetch six independent pieces of data (simulated here by six network requests). The sequential approach looks like this:

Task { let startInstant = clock.now
    // Each 'await' pauses execution until the previous one is complete
    let joke1 = try! await requestJoke(index: 1)
    let joke2 = try! await requestJoke(index: 2)
    let joke3 = try! await requestJoke(index: 3)
    let joke4 = try! await requestJoke(index: 4)
    let joke5 = try! await requestJoke(index: 5)
    let joke6 = try! await requestJoke(index: 6)

    jokes = [joke1, joke2, joke3, joke4, joke5, joke6]
    print("Time taken = \(clock.now - startInstant)")
}

private func requestJoke(index: Int) async throws -> String { 
    let url = URL(string: "https://api.chucknorris.io/jokes/random" )! 
    let (data, _) = try await URLSession.shared.data(from: url)
    let decoded = try JSONSerialization.jsonObject(with: data, options: []) print("(index)") 
    if let dict = decoded as? [String: Any] { 
        return dict["value"] as! String 
    } 
    return "Error" 
}

In a test environment, this sequential execution took approximately 2.2 to 2.3 seconds to complete all six requests. The total time is roughly the sum of the individual request times, as they run one after the other.

Achieving True Parallelism with async let

The async let binding is designed to solve this exact problem. It allows you to declare an asynchronous value, immediately starting the task in parallel with the current execution flow. The task runs concurrently in the background, and the current task continues without pausing. You only use await later when you actually need the result of that task.

By using async let, all six network requests are initiated almost simultaneously, and the program only pauses at the final step to collect the results.

Task { let startInstant = clock.now
    // Each 'await' pauses execution until the previous one is complete
    async let joke1 = try! requestJoke(index: 1)
    async let joke2 = try! requestJoke(index: 2)
    async let joke3 = try! requestJoke(index: 3)
    async let joke4 = try! requestJoke(index: 4)
    async let joke5 = try! requestJoke(index: 5)
    async let joke6 = try! requestJoke(index: 6)

    // The 'await' here waits for all six concurrent tasks to finish
    jokes = await [try! joke1, try! joke2, try! joke3, try! joke4, try! joke5, try! joke6]
    print("Time taken = \(clock.now - startInstant)")
}

The performance improvement is dramatic. In the same test environment, the parallel execution using async let completed in approximately 1.0 to 1.1 seconds. This is a reduction of over 50% in execution time, as the total time is now dictated by the longest-running task, not the sum of all tasks.

Summary of Performance

The following table summarizes the performance difference between the two approaches:

Execution MethodTask InitiationTotal Execution Time (Approx.)Performance Gain
Sequential awaitOne task starts after the previous one finishes.2.2 - 2.3 secondsBaseline
Parallel async letAll tasks start concurrently.1.0 - 1.1 seconds~55% Faster

Conclusion

For any Swift application that needs to perform multiple independent asynchronous operations, adopting async let is a straightforward and highly effective way to leverage the power of structured concurrency and achieve significant performance gains. It provides a clean, readable, and modern alternative to older techniques like DispatchGroup, ensuring your application remains responsive and efficient.

By replacing sequential await calls with async let for independent tasks, you can ensure that your application is utilizing system resources to their fullest, leading to a faster and more fluid user experience.