01. Async and Await

It will take about 10 minutes to finish reading this article.

1. Description

In Swift, async and await are keywords used for asynchronous programming, a cleaner, more readable way to handle asynchronous tasks. These are new features introduced in Swift 5.5 that make asynchronous code look more like synchronous code, and the use of async/await also naturally preserves the semantic structure of the code, providing the necessary support for at least three horizontal improvements:
(1) Better performance for asynchronous code.
(2) Better tools that provide a more consistent experience when debugging, profiling, and exploring code.
(3) A foundation for future concurrency features.
In short, they improve the maintainability and understandability of code.

2. How to use async and await

2.1 async
A function can be declared as asynchronous by prefixing the return arrow of the function declaration with the async keyword:

1
2
3
4
5
6
7
8
func loadSignature() async throws -> String {
do {
let signature = try await simulateSignatureLoading()
return signature
} catch {
throw CustomError.loadingError
}
}

An asynchronous function can pause execution when it encounters the await keyword and wait for an asynchronous operation to complete before resuming execution.

2.2 await
Await is a keyword used to call asynchronous methods. When await is encountered, the execution of the function pauses until the asynchronous operation completes and returns the result.
Here’s an example of using await to call an asynchronous function and wait for its result:

1
2
3
4
5
6
7
8
9
10
11
12
13

func simulateSignatureLoading() async throws -> String {
// Simulate some asynchronous loading process, eg, from a network request or a file.
await Task.sleep(2_000_000_000) // Simulate a 2-second delay

let success = Bool.random() // Simulate successful or unsuccessful loading
if success {
let signature = "This is my signature"
return signature
} else {
throw CustomError.signatureNotFound
}
}

Suspension mentioned above:
(1) The suspension is the method, not the thread that executes the method.
(2) Accurately await should be called a potential suspension point, not a suspension point, because not all await will be suspended, only encountered similar IO, manually start a sub-thread, etc. will suspend the current call stack.
(3) Before and after the pause point may occur before and after the thread switch, it is because of the asynchronous method before and after the pause point may change the thread of execution, so in the asynchronous method should be careful to use locks, semaphores and other synchronization operations.

1
2
3
4
5
6
7
8
9
10
11
12
let lock = NSLock.init()
func test() async {
lock.lock()
try? await Task.sleep(nanoseconds: 1_000_000_000)
lock.unlock()
}

for i in 0..<10 {
Task {
await test()
}
}

Code like the above generates a deadlock at lock.lock(), as does switching to a semaphore.
Note:
async-await is often used companionally in Swift and can be thought of as simply this:
“await is always waiting for a response from its partner, async.”

3. Application of async-await

3.1 Replaces traditional closure callbacks (Asynchronous serial)
Traditional closure callbacks are common in Swift for returning from an asynchronous task, usually in combination with a parameter of type result, as follows:

1
2
3
func fetchImages(completion: ([UIImage]?, Error?) -> Void) {
// .. Execution of data requests
}

Or

1
2
3
4

func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. Execution of data requests
}

While this approach is still common, it can have several drawbacks:
(1) You have to make sure you call completion closures in every possible exit method.
(2) Closure code is often too deeply nested, optional types must be unwrapped, etc., making it harder to read and debug.
(3) You need to use weak references to avoid circular references.
(4) Implementers need to switch on results to get results (Block callbacks) and cannot use try catch statements from the implementation level.
The fetchImages function above can simply be replaced with async/await as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func fetchImages() async throws -> [UIImage] {
// Simulate asynchronous operations, such as requesting image data from the web
return await withCheckedThrowingContinuation { continuation in
DispatchQueue.global().async {
// Perform an asynchronous operation to get images and errors
let images: [UIImage] = []
let error: Error? = nil

if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: images)
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

async func imagesData() {
do {
let images = try await fetchImages()
// This is where the fetched images are processed
} catch {
// Handling error
}
}

Task {
await imagesData()
}

Note 1:
When adopting async-await in an existing project, you should be careful not to break all the code at once. When doing such a large-scale refactoring, it’s best to consider maintaining the old implementation for the time being so that you don’t have to update all the code until you know if the new implementation is stable enough.
Example:

1
2
3
4
5
6
7
8
9
struct ImageFetcher {
@available(*, renamed: "fetchImages()")
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {

}
func fetchImages() async throws -> [UIImage] {
// .. Execution of data requests
}
}

The asynchronous alternative refactoring option is added here, which still retains the old implementation, but adds an available attribute. If you want Xcode’s deprecation warning, you can change it to:

1
@available(*, deprecated, renamed: "fetchImages()")

This way you can optimize your code incrementally without having to refactor your entire project at once.

Note 2:
In the above code, the Result enumeration is also optimized together. Yes, async-await can greatly reduce the use of Result enumerations.

3.2 async-let (asynchronous parallelism)
Let’s take a look at the following application for async-await:

1
2
3
4
5
6
7
8
9
10
11
func fetchImage(from url: URL) async throws -> UIImage {
// Simulate asynchronous downloading of images
print("fetchImage----- begin \(Thread.current)")
await Task.sleep(1_000_000_000) // Simulates 1 second download time
let data = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw NSError(domain: "ImageDownloadError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to download image"])
}
print("fetchImage----- end \(Thread.current)")
return image
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func downloadImages() async {
let imageURLs = [
URL(string: "https://example.com/image1.jpg")!,
URL(string: "https://example.com/image2.jpg")!,
URL(string: "https://example.com/image3.jpg")!
]

do {
let image1 = await fetchImage(from: imageURLs[0])
print("Downloaded image 1:", image1)
let image2 = await fetchImage(from: imageURLs[1])
print("Downloaded image 2:", image2)
let image3 = await fetchImage(from: imageURLs[2])
print("Downloaded image 3:", image3)

} catch {

}
}

Task {
await downloadImages()
}

The 3 images in the example are downloaded serially one by one, using async-let can optimize this code for asynchronous parallelism:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async func downloadImages() {
let imageURLs = [
URL(string: "https://example.com/image1.jpg")!,
URL(string: "https://example.com/image2.jpg")!,
URL(string: "https://example.com/image3.jpg")!
]
var images: [UIImage] = []
do {
print(">>>>>>>>>> 1 \(Thread.current)")
async let fetchImage1 = fetchImage(from: imageURLs[0])
async let fetchImage2 = fetchImage(from: imageURLs[1])
async let fetchImage3 = fetchImage(from: imageURLs[2])
print(">>>>>>>>>> 2 \(Thread.current)")
let (image1, image2, image3) = await (try fetchImage1, try fetchImage2, try fetchImage3)
print(">>>>>>>>>> 3 \(Thread.current)")
images.append(contentsOf: [image1, image2, image3])
} catch {

}
}

The result of the Print:

1
2
3
4
5
6
7
8
9
10
11
>>>>>>>>>> 1 <_NSMainThread: 0x7b10000003c0>{number = 1, name = main}
>>>>>>>>>> 2 <_NSMainThread: 0x7b10000003c0>{number = 1, name = main}
fetchImage----- begin <NSThread: 0x7b1000043a40>{number = 6, name = (null)}
fetchImage----- begin <NSThread: 0x7b1000046200>{number = 7, name = (null)}
fetchImage----- begin <NSThread: 0x7b1000046200>{number = 7, name = (null)}
fetchImage----- begin <NSThread: 0x7b1000043a40>{number = 6, name = (null)}
fetchImage----- end <NSThread: 0x7b100004b700>{number = 3, name = (null)}
fetchImage----- end <NSThread: 0x7b1000048ac0>{number = 4, name = (null)}
fetchImage----- end <NSThread: 0x7b100004b700>{number = 3, name = (null)}
fetchImage----- end <NSThread: 0x7b1000046200>{number = 7, name = (null)}
>>>>>>>>>> 3 <_NSMainThread: 0x7b10000003c0>{number = 1, name = main}

Note:
After modifying the image with async-let, fetchImage hangs, and the thread continues to perform other tasks until it encounters await, and fetchImage will not execute, which is why print2 executes before fetchImage. So async-let is a concurrent binding mechanism to achieve asynchronous concurrency.

4. Conversion between asynchronous and synchronous functions

Asynchronous function types differ from synchronous function types. However, there exists an implicit conversion from a synchronous function type to the corresponding asynchronous function type. The approximate rules for combining ‘throw’ are as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct FunctionTypes {
var syncNonThrowing: () -> Void
var syncThrowing: () throws -> Void
var asyncNonThrowing: () async -> Void
var asyncThrowing: () async throws -> Void

mutating func demonstrateConversions() {
// Okay to add 'async' and/or 'throws'
asyncNonThrowing = syncNonThrowing
asyncThrowing = syncThrowing
syncThrowing = syncNonThrowing
asyncThrowing = asyncNonThrowing

// Error to remove 'async' or 'throws'
syncNonThrowing = asyncNonThrowing // error
syncThrowing = asyncThrowing // error
syncNonThrowing = syncThrowing // error
asyncNonThrowing = syncThrowing // error
}
}

As you can see above, basically synchronous functions can be converted to asynchronous functions, but it doesn’t work the other way around.

Reference

[1] https://cloud.tencent.com/developer/article/2191310
[2] https://juejin.cn/post/7025261081291407373
[3] https://juejin.cn/post/7076733264798416926
[4] https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md