WWDC21-Concurrency-AsyncAwait

1.Async/await

  • 异步闭包的缺点:要保证方法中的每个分支都要执行闭包
    • swift能保证有返回值的函数,如果没有返回某个值,就会抛出异常(这个工作可以由swift来做)
    • 但是异步闭包却无法保证在每个分支中都执行闭包
    • async/await可以解决这个问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 异步闭包
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
completion(nil, FetchError.badImage) //容易忘记
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage) //容易忘记
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
// async/await:成功会返回UIImage,失败会抛出异常
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
//此处会把该方法挂起,线程可以处理其他操作,等获取data成功之后,再继续进行下面的操作
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage }
return thumbnail
}
extension UIImage {
var thumbnail: UIImage? {
get async { //变量也可以是async,要求:1.显式get,2.不能有set
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}

2.Async sequences

  • async sequences和普通的sequence的区别是,他取出元素时是异步的。
1
2
3
4
5
for await id in staticImageIDsURL.lines {
let thumbnail = await fetchThumbnail(for: id)
collage.add(thumbnail)
}
let result = await collage.draw()

3.Async/await原理

3.1 普通的函数调用

image-20210627173115822

3.2 async函数调用

  • await会将线程挂起,把函数的运行交给系统来控制(而不是线程),由系统决定什么时候执行;
  • 执行完成之后,系统会调用resume将结果返回给函数。
  • async函数中可以多次挂起,也可以一次都不挂起

image-20210627173745795

await意味着该函数不是在一个事务里进行的,在await之后线程可能会做很多其他事情,甚至切换到另外一个线程。(详细信息可以观看“protect mutable state with swift actors”)

4.异步方法的测试

4.1使用XCTestExpectation

1
2
3
4
5
6
7
8
9
10
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() throws {
let expectation = XCTestExpectation(description: "mock thumbnails completion") // 1.创建expectation
self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
XCTAssertNil(error)
expectation.fulfill() // 2.fulfill
}
wait(for: [expectation], timeout: 5.0) // 3.wait
}
}

4.2使用Async/await

1
2
3
4
5
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() async throws {
XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
}
}

5.sync转async

  1. 移除completion闭包
  2. 函数签名后面加上async
  3. 闭包调用处加上await或者try await
  4. 用async{ } 将await包起来
1
2
3
4
5
6
7
8
9
10
struct ThumbnailView: View {
var body: some View {
Image(uiImage: self.image ?? placeholder)
.onAppear {
async { // 可以在非async方法中调用
self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
}
}
}
}

6.Async alternatives 和 continuation

  • Async alternatives: 函数有可能返回两种不同类型的值
  • continuation:手动控制函数返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Existing function
func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) {
do {
let req = Post.fetchRequest()
req.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
completion(result.finalResult ?? [], nil)
}
try self.managedObjectContext.execute(asyncRequest)
} catch {
completion([], error)
}
}

// Async alternative
func persistentPosts() async throws -> [Post] {
typealias PostContinuation = CheckedContinuation<[Post], Error>
return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
self.getPersistentPosts { posts, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: posts)
}
}
}
}
  • 有些情况需要不同的代理方法返回的数据,此时需要把continuation存储起来,每次使用时赋值,使用完成清除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ViewController: UIViewController {
private var activeContinuation: CheckedContinuation<[Post], Error>?
func sharedPostsFromPeer() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuation in
self.activeContinuation = continuation // 赋值continuation
self.peerManager.syncSharedPosts() // 调用代理方法
}
}
}

extension ViewController: PeerSyncDelegate {
func peerManager(_ manager: PeerManager, received posts: [Post]) {
self.activeContinuation?.resume(returning: posts)
self.activeContinuation = nil // guard against multiple calls to resume
}

func peerManager(_ manager: PeerManager, hadError error: Error) {
self.activeContinuation?.resume(throwing: error)
self.activeContinuation = nil // guard against multiple calls to resume
}
}
0%