Sequential vs. Concurrent Operations with Async / Await in Swift

In my previous article on structured concurrency with Swift, I highlighted many new asynchronous programming concepts anticipated with Swift 5.5. New keywords like Actors, MainActor, Async, and Await, will quickly be used in regular Swift applications, helping streamline complex operations and increase efficiency.

What's nice about the new model is that it provides a great platform to express one's ideas clearly, depending on the data scenario. To illustrate, let's review how one can use async/await commands to replicate sequential versus concurrent operations. To start, let's write sample code for retrieving photos from an external service:


class Photos {
                
    func getPhoto(inGallery name: String) async -> String {
                
        for _ in 1...10 {
            print("sleeping for \(name)")
            await Task.sleep(20)
        }
        
        return name + "_IMG001"
    }   
}

Like this article? Get more content like this when you sign-up for the weekly iOS Computer Science Lab.

As we can see, the type contains a single async method named getPhoto. Also, notice the type isn't an Actor but is a (regular) class. To simulate the idea of a long-running process, we've also added a sleep command in the method-body.  

Sequential Photo Access

With our Photo's class in place, let's review how we can execute the getPhotos method as series of sequential requests. As the name goes, the idea of sequential access implies lines of code get called in a specific order. In other words, one line of code won't start until the previous line of code completes. 

    func testGroupPhotosSequential() {
        
        //non-actor instance
        let p = Photos()
        
        async {
                        
            //running processes sequentially
            let wayne = await p.getPhoto(inGallery: "Wayne")
            let sam = await p.getPhoto(inGallery: "Sam")
            
            let items = [wayne, sam]
        }
    }

What makes the async/await model flexible is how the keywords can provide different levels of functionality depending on their placement. In this example, the unit test and the invocation of Photos works within a regular synchronous context. However, since our getPhotos method is async, it must be called using the (new) async{} closure.  

Note how both lines of code get prefixed with await when calling the getPhotos method. This syntax implies an additional thread of execution may have to wait to fulfill its request. However, in this case, the assignment to sam will need to wait until the assignment to wayne completes. 


Design Goals

While the code is technically correct, let's reconsider the function's primary goal. Both results from the call to getPhoto get used to populate a collection (item). When or how variables get assigned to the group immaterial. If redesigned, this means calls to getPhotos could also occur in parallel, potentially increasing the algorithm's performance. As a result, let's redesign our method so that calls to getPhoto occur concurrently versus sequentially:

    func testGroupPhotosConcurrent() async {
        
        //non-actor instance
        let p = Photos()
                
        //now we are running multiple concurrent processes
        async let wayne = p.getPhoto(inGallery: "Wayne")
        async let sam = p.getPhoto(inGallery: "Sam")
        
        let items = await [wayne, sam]
    }

Concurrent Photo Access

Our new function still calls getPhoto but now works differently. First, note how the method signature gets marked with async. The entire procedure now runs as an asynchronous (background) process. Also, the await prefix to getPhotos is removed. In its place, the variable assignments to sam and wayne are prefixed with the new async-let syntax. This will allow the variable assignment to occur in a non-awaiting context. 

Finally, the items' collection initializer is now prefixed with await. This makes sense, as we can't guarantee that entities will obtain all their required data but can be assured program execution won't move forward until all exceptions have been met. 

What's Next

With Xcode 13 currently in beta with its anticipated release this fall, there's undoubtedly much more to explore, but it's fabulous to see these welcome changes to the iOS ecosystem.