read

This is yet another way to do concurrency, this time in Swift 5.5 with much cleaner code. First introduced in WWDC 2021, but it won’t be the last we see of it.

Implement async functions

Simply declare with the async keyword. It is also common to have throws right after it (so as to throw error instead of returning in the old Result tuple).

func asyncHello() async throws -> String {
  await Task.sleep(1_000_000_000)  // 1 second
  return "Hello World"
}

It is also common to replace your existing functions with completion handlers by using withCheckedContinuation/withUnsafeContinuation.

func newAsyncFunc() async throws -> String {
  await withCheckedContinuation { continuation in
    existingOldFunc { (items, error) in
      if let error = error {
        continuation.resume(throwing: error)
      } else {
        continuation.resume(returning: items)
      }
    }
  }
}

Use await to call async functions

do {
  let result = try await asyncHello()
} catch {
  log(error)
}

The keyword await indicates there is possible suspension. To be able to suspend execution, only certain part of the code can call it, namely:

  • async function
  • @main, @MainActor
  • Detached Task

Sequentially or in parallel

The beauty is that you can have multiple await calls, and they will run line after line, sequentially.

let result1 = try await asyncHello() //
let result2 = try await asyncHello() // Runs after result1 yield

To run in parallel, use async let.

async let result1 = try downloadPhoto(1)
async let result2 = try downloadPhoto(2)
let results = await [result1, result2]

async-let flow

AsyncSequence

This is for sequence of values arriving over time. Sounds familiar? Yes, the use case is similar to Combine.

You iterate an AsyncSequence as per normal, but with an await.

for await str in asyncLinesOfText {
    ...
}

Structured Currency

It is structured because each Task can have child tasks – they have explicit relationships.

You can also detach task, and becomes unstructured.

A TaskGroup can create dynamic number of task. An example from WWDC:

Task Group

Properties of tasks

actor

actor is similar to class, except it allows only one task to access the mutable state at a time, which makes it safe for code in multiple tasks.

If you access an actor property from the outside, you need to use await.

A useful global actor wrapper is @MainActor. Declare a func with it, and the func will run on main thread.

Running on main thread

There are a few ways to ensure your code is on the main thread.

  • Annotate @MainActor on a class or func
  • Task { @MainActor in ... }
  • await MainActor.run { ... }

Running on non-main thread

One way is to use Task.detached { ... }. However, note that detached task is unstructured concurrency, which means you need to handle cancellation yourself. It is a “last resort”.

If you need to perform some heavy work on non-main thread, use good old DispatchQueue. You can have an async func that runs on a custom serial queue like this:

private let queue = DispatchQueue(label: "my.serial.queue", qos: .userInitiated)

func doLongRunningOnQueue() async -> String {
    await withCheckedContinuation { continuation in
        queue.async {
            ...
            continuation.resume(returning: "foo")
        }
    }
}

This is the way until we can specify the executor for a Task.

Switching task

You can call await Task.yield() to allow the current task to switch to another task. By yielding, you can ensure your queue won’t be blocking.


Image

@samwize

¯\_(ツ)_/¯

Back to Home