8

Async Await in Swift

 3 years ago
source link: https://auth0.com/blog/async-await-in-swift/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

This year, WWDC came with a bunch of new features and updates. Maybe one of the most expected was the introduction of the new concurrency system by using async/await syntax. This is a huge improvement in the way that we write asynchronous code.

Before Async/Await

Imagine that we are working on an app for a grocery store and, we want to display its list of products. We are probably going to have something like this:

func fetchProducts(_ completion: @escaping([Product]) -> Void) {...}

var products = [Product]()
fetchProducts { [weak self] products in
    guard let strongSelf = self else { return }
    strongSelf.products.append(contentsOf: products)
}

A pretty standard and well-known code using completion blocks. Now suppose that the grocery store has, once in a while, some kind of offers for some products (e.g., "Take 2, pay 1"). And, we want to hold a list with these offers. Let's adjust our code by creating a new function to retrieve a String with the promotion text, given a specific product.

func fetchProducts(_ completion: @escaping([Product]) -> Void) {...}
func getOffer(for product: Int, @escaping(String) -> Void) {...}

typealias ProductOffer = (productId: Int, offer: String)
var products = [Product]()
var offers = [ProductOffer]()

fetchProducts { [weak self] products in
    guard let strongSelf = self else { return }

    for product in products {
        strongSelf.products.append(product)

        getOffer(for: product.id) { [weak self] offerText in
            guard let strongSelf = self else { return }
            let productOffer = ProductOffer(productId: product.id, offer: offerText)
            strongSelf.offers.append(productOffer)
        }
    }
}

We only have two nested closures for a simple feature, and you can see that our code starts to get a little messed up.

Async/Await

From Swift 5.5 onwards, we can start using async/await functions to write asynchronous code without using completion handlers to returns values. Instead, we are allowed to return the values in the return object directly.

To mark a function as asynchronous, we only have to put the keyword async before the return type.

func fetchProducts() async -> [Product] {...}
func getOffer(for product: Int) async -> String {...}

This is much easier and simple to read, but the best part comes from the caller's side. When we want to use the result of a function marked as async, we need to make sure that its execution is already completed. To make this possible, we need to write the await keyword in front of the function call. By doing this, the current execution will be paused until the result is available for its use.

let products = await fetchProducts()

for product in products {
    let offerText = await getOffer(for: product.id)

    if !offerText.isEmpty {
        let productOffer = ProductOffer(productId: product.id, offer: offerText)
        offers.append(productOffer)
    }
}

Although, if we want to execute other tasks while the async function is being executed, we should put the keyword async in front of the variable (or let) declaration. In this case, the await keyword will need to be placed in front of the variable (or let) where we are accessing the result of the async function.

async let products = fetchProducts()
...
// Do some work
...
print(await products)

Parallel Asynchronous Functions

Now imagine that in our app, we want to fetch products by category—for example, just the frozen products. Let's go ahead and make the adjustments to our code.

enum ProductCategory {
    case frozen
    case meat
    case vegetables
    ...
}

func fetchProducts(fromCategory category: ProductCategory) async -> [Product] {...}

let frozenProducts = await fetchProducts(fromCategory: .frozen)
let meatProducts = await fetchProducts(fromCategory: .meat)
let vegetablesProducts = await fetchProducts(fromCategory: .vegetals)

This is ok, but the code will run in serial mode, which means that we won't start fetching the meat products until the frozen products are retrieved. Same for the vegetables. Remember, we write the await keyword if we want to pause our execution until the function completes its work. However, in this particular scenario, we could start fetching the three categories at the same time, running in parallel.

In order to accomplish this, we need to write the async keyword in front of the var (or let) declaration and use the await keyword when we want to use it.

async let frozenProducts = await fetchProducts(fromCategory: .frozen)
async let meatProducts = await fetchProducts(fromCategory: .meat)
async let vegetablesProducts = await fetchProducts(fromCategory: .vegetables) 

....

let products = await [frozenProducts, meatProducts, vegetablesProducts]

Error handlers

Our fetching functions might have some errors that make it impossible to return the expected data values. How do we handle this in our async/await context?

We have a couple of options. The first one is to return the well-known Result object.

func fetchProducts() async -> Result<[Product], Error> {...}

let result = try await fetchProducts()
switch result {
    case .success(let products):
        // Handle success
    case .failure(let error):
        // Handle error
}

Another one is to use the try/catch approach.

func fetchProducts() async throws -> [Product[ {...}
...
do {
    let products = try await fetchProducts()
} catch {
    // Handle the error
}

The main benefit that we had when using the Result type was to improve our completion handlers. In addition to that, we got a cleaner code at the moment we used the result, being able to switch between success and failure cases.

On the other hand, the use of throw errors adds extra readability in the function's definition because we only need to put the result type that the function will return. The errors handling is hidden in the function's implementation.

Asynchronous Sequences

Let's say that we have a requirement to load a list of products from some .csv file. A traditional way to do this is to load all the lines at once and then start processing them. But, what happens if we want to start doing some work as soon as we have one of the lines available? We can now do this using an asynchronous sequence.

let url = URL(string: "http://www.grocery.com/products.csv")
for try await in url.lines {
    // Do some work
}

Using this new feature also allows us to handle this particular case (reading a file) in a simpler way than before. You can check this stackoverflow discussion to see how we had to do this and see the advantages that this approach has over the previous one.

Async/Await vs. Completion Handlers

As we saw in the previous sections, the use of async/await syntax comes with a lot of improvements in contrast with using completion blocks. Let's make a quick recap.

Advantages

  • Avoid the Pyramid of Doom problem with nested closures
  • Reduction of code
  • Easier to read
  • Safety. With async/await, a result is guaranteed, while completion blocks might or might not be called.

Disadvantages

  • It's only available from Swift 5.5 and iOS 15 onwards.

Actors

Take a look at the following example, just a simple Order class in which we will be adding products and eventually make the checkout.

class Order {
    
    var products = [Product]()
    var finalPrice = 0

    func addProduct(_ product: Product) {
        products.append(product)
        finalPrice += product.price
    }
}

If we are in a single-thread application, this code is just fine. But what happens if we have multiple threads that can access our order's final price?

  1. We are on the product list and add some specific products to our order. The app will call the addProduct function.
  2. The product is added to our order's product list
  3. Before the final price gets updated, the user tries to checkout.
  4. The app will read the final price of our order
  5. The addProduct function completes and updates the final price. But the user already checkout and paid less than they should.

This problem is known as Data Races when some particular resource could be accessed from multiple parts of the app's code.

Actors, also introduced in Swift 5.5 and iOS 15, resolve this problem for us. An Actor is basically like a class but with a few key differences that make them thread-safe:

  • Only allow accessing their state by one task at a time
  • Stored properties and functions can only be access from outside the Actor if the operation is performed asynchronously.
  • Stored properties can't be written from outside the Actor.

On the downside:

  • Actors do not support inheritance

You can think about the Actors like a similar solution of the semaphores) concept.

To create one, we just need to use the actor keyword.

actor Order {
    
    var products = [Product]()
    var finalPrice = 0

    func addProduct(_ product: Product) {
        products.append(product)
        finalPrice += product.price
    }
}

And we can create an instance using the same initializer syntax as structures and classes. If we want to access the final price, we must do it using the keyword await (because outside the actor's scope, we are only allowed to access the properties asynchronously).

print(await order.finalPrice)

Conclusion

Definitely async/await brings to the table an easier way to write asynchronous code, removing the need to use completion blocks. In addition, we get more readable and flexible code if our application starts scaling up.

However, the minimum iOS deployment target will be an entry barrier for most of us unless you start a project from scratch, in which case is highly recommended to wait until the official release of iOS 15 + Xcode 13 + Swift 5.5 to take full advantage of the new concurrency system.


Recommend

  • 126
    • 微信 mp.weixin.qq.com 7 years ago
    • Cache

    浅谈Async/Await

    在很长一段时间里面,FE们不得不依靠回调来处理异步代码。使用回调的结果是,代码变得很纠结,不便于理解与维护,值得庆幸的是Promise带来了.then(),让代码变得井然有序,便于管理。于是我们大量使用,代替了原来的回调方式。但是不存在一种方法可以让当前的执行流...

  • 76
    • beckjin.com 7 years ago
    • Cache

    ASP.NET 谨用 async/await

    ASP.NET 谨用 async/await

  • 75

    C# 5.0 引入 async/await 关键字,旨在简化异步编程模型,抛去语法糖就是 Net4.0 的 Task + 状态机。其实在处理异步编程使用 Task 还是挺简单的,不过既然推出了新的语法糖,难免会尝试一下,然而在使用中却没想象中那么单纯。以下针对ASP.NET 应用程序实际使用过...

  • 15

    You all might know that async/await is accepted and is available in the main snapshots! Let’s get our hands dirty by trying out some basic example of async/await in Swift. Prerequisites Xcode 12.3 La...

  • 6

    Swift 5.5 Brings Async/Await and Actor Support Jun 13, 2021 2...

  • 11

    Making Network Requests with Async/await in Swift Traditionally, when we want to make a network request, we must use the closure-based URLSession APIs to perform the request asynchronously so that our apps can be respons...

  • 6

    Using Async/Await with AWS Amplify Libraries for Swift by Kyle Lee | on 03 NOV 2022 | in

  • 7

    Swift 中的 Async/Await ——代码实例详解 作者:Swift君 2022-11-21 09:01:00 Swift 中的 async-await 允许结构化并发,这将提高复杂异步代码的可读性。不再需要完成闭包,而在彼此之后调用多个异步方法的可读性也...

  • 6

    Swift并发编程 - 理解 async 和 await 原创  2023-06-30  本文是我学习 Swift 并发编程的第一篇笔记,文章从几个不太好理解的点,介绍了async 和 await 语法关键字的使用方法和内在含义。 async 使用 async 修...

  • 13

    How async/await works internally in Swift How async/await works internally in Swift Published on 28 Sep 2023 async/await in Swift was introduced with iOS 15, and...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK