即使关于经验丰富的开发者来说,写出健壮性、可保护性高的并发代码也是一项具有应战性的任务,其应战首要体现在两个方面:

  • 传统并发模型是依据异步形式,代码保护性不行友爱;

  • 并发往往意味着 Data Races,这是一类难复现、难排查的常见问题。

Swift 在 5.5 开端引入的新并发结构首要着力处理这 2 个问题。

本文是 『 Swift 新并发结构 』系列文章的第一篇,首要介绍 Swift 5.5 引入的 async/await。

本系列文章对 Swift 新并发结构中触及的内容逐一进行介绍,内容如下:

  • Swift 新并发结构之 async/await

  • Swift 新并发结构之 actor

  • Swift 新并发结构之 Sendable

  • Swift 新并发结构之 Task

本文一同宣布于我的个人博客

Overview


在正式开端前,简略回忆一下同步/异步、串行/并行的概念:

  • 同步(Synchronous)、异步(Asynchronous) 通常指方法(/函数),同步方法标明直到任务完成才回来,异步方规律是将任务抛出去,在任务完成前就回来;

    这也就意味着需求通过某种方法取得异步任务的成果,如:Delegate、Closure 等。

  • 串行(Serial)、并行(Concurrent) 通常指 App 实行一组任务的形式,串行标明一次只能实行一个任务,只需其时任务完成后才启动下一个任务,而并行指可以一同实行多个任务。最常见的莫过于 GCD 中的串行、并行队列;

    ps. 在此我们不严厉差异并发、并行的差异。

  • 传统的并发模型都是依据异步形式的,即异步获取并发任务的成果。

同步代码是线性的 (straight-line),十分适宜人脑处理。

而异步代码对错线性的、跳跃式的 (类似于 goto 语句),关于单核的人脑来说是一大应战。

除了在阅读上对人脑思想形式构成较大应战外,异步代码在详细完成上常伴有以下问题:

  • 回调阴间 (Callback Hell);

  • 差错处理 (Error Handling);

  • 简略犯错。

初探


我们先通过一个简略的比方比照一下传统并发模型与新的并发模型间的差异。

该比方通过 token 获取头像,其过程有:

  • 通过 token 获取头像 URL;

  • 通过 URL 下载头像数据(加密);

  • 对头像数据解密;

  • 图片解码。

class AvatarLoader {
  func loadAvatar(token: String, completion: (Image) -> Void) {
    fetchAvatarURL(token: token) { url in
      fetchAvatar(url: url) { data in
        decryptAvatar(data: data) { data in
          decodeImage(data: data) { image in
            completion(image)
          }
        }
      }
    }
  }
  func fetchAvatarURL(token: String, completion: (String) -> Void) {
    // fetch url from net...
    completion(avatarURL)
  }
  func fetchAvatar(url: String, completion: (Data) -> Void) {
    // download avatar data...
    completion(avatarData)
  }
  func decryptAvatar(data: Data, completion: (Data) -> Void) {
    // decrypt...
    completion(decryptedData)
  }
  func decodeImage(data: Data, completion: (Image) -> Void) {
    // decode...
    completion(avatar)
  }
}

loadAvatar 方法中回调层级之深显而易见。

上述代码还遗漏了一个重要问题:差错处理,其间的网络请求、解密、解码都有或许犯错。

高雅地处理差错是一项十分检测底子功的任务。

一般地,差错处理分为 2 种情况:

  • 同步方法:优先考虑通过 throw 抛出error,这样调用方就不得不处理差错,因而带有必定的强制性;

  • 异步方法:在回调中传递 error,这种情况下调用方通常会有意无意地疏忽差错,使健壮性大打折扣。

为了处理差错,对上述代码进行升级:

class AvatarLoader {
  func loadAvatar(token: String, completion: (Image?, Error?) -> Void) {
    fetchAvatarURL(token: token) { url, error  in
      guard let url = url else {
        // 在这个途径,常常简略漏掉实行 completion 或者 return 语句
        completion(nil, error)
        return
      }
      fetchAvatar(url: url) { data, error in
        guard let data = data else {
          completion(nil, error)
          return
        }
        decryptAvatar(data: data) { data, error in
          guard let data = data else {
            completion(nil, error)
            return
          }
          decodeImage(data: data) { image, error in
            completion(image, error)
          }
        }
      }
    }
  }
  func fetchAvatarURL(token: String, completion: (String?, Error?) -> Void) {
    // fetch url from net...
    completion(avatarURL, error)
  }
  func fetchAvatar(url: String, completion: (Data?, Error?) -> Void) {
    // download avatar data...
    completion(avatarData, error)
  }
  func decryptAvatar(data: Data, completion: (Data?, Error?) -> Void) {
    // decrypt...
    completion(decryptedData, error)
  }
  func decodeImage(data: Data, completion: (Image?, Error?) -> Void) {
    // decode...
    completion(avatar, error)
  }
}

可以看到,为了处理差错,在 completion 中增加了 error 参数,一同需求将 2 个参数都界说成 Optional

一同,在 loadAvatar 中添加了许多的 guard,这样的代码无疑十分丑恶。

Optional 无形中增加了代码本钱。

为此,Swift 5 引入了 Result 用于优化上述差错处理场景:

class AvatarLoader {
  func loadAvatar(token: String, completion: (Result<Image, Error>) -> Void) {
    fetchAvatarURL(token: token) { result in
      switch result {
      case let .success(url):
        fetchAvatar(url: url) { result in
          switch result {
          case let .success(decryptData):
            decryptAvatar(data: decryptData) { result in
              switch result {
              case let .success(avaratData):
                decodeImage(data: avaratData) { result in
                  completion(result)
                }
              case let .failure(error):
                completion(.failure(error))
              }
            }
          case let .failure(error):
            completion(.failure(error))
          }
        }
      case let .failure(error):
        completion(.failure(error))
      }
    }
  }
  func fetchAvatarURL(token: String, completion: (Result<String, Error>) -> Void) {
    // fetch url from net...
    completion(.success(avatarURL))
  }
  func fetchAvatar(url: String, completion: (Result<Data, Error>) -> Void) {
    // download avatar data...
    completion(.success(avatarData))
  }
  func decryptAvatar(data: Data, completion: (Result<Data, Error>) -> Void) {
    // decrypt...
    completion(.success(decryptData))
  }
  func decodeImage(data: Data, completion: (Result<Image, Error>) -> Void) {
    // decode...
    completion(.success(avatar))
  }
}

Result 是 enum 类型,含有 successfailure 2 个 case。

可以看到,通过运用 Result,参数不必是 Optional,另外可以通过 switch/case 来处理成果,在必定程度保证了调用方对差错的处理。

在上面这个 Callback Hell 中,直观上, Result 不但没有使代码简洁,反而愈加杂乱了。

首要是没有把代码抽离开来,不要对 Result 有什么误解^__^。

通过这个简略的比方,可以看到依据 Callback 的异步模型问题不少。

因而,将异步代码同步化一直是业界极力的方向。

如:Promise,不过其同步也是建立在 callback 基础上的。

Swift 5.5 引入了 async/await 用于将异步代码同步化。

许多言语都已支撑 async/await,如: JavaScript、Dart 等

先直观感受一下 async/await

class AvatarLoader {
  func loadAvatar(token: String) async throws -> Image {
    let url = try await fetchAvatarURL(token: token)
    let encryptedData = try await fetchAvatar(url: url)
    let decryptedData = try await decryptAvatar(data: encryptedData)
    return try await decodeImage(data: decryptedData)
  }
  func fetchAvatarURL(token: String) async throws -> String {
    // fetch url from net...
    return avatarURL
  }
  func fetchAvatar(url: String) async throws -> Data {
    // download avatar data...
    return avatarData
  }
  func decryptAvatar(data: Data) async throws -> Data {
    // decrypt...
    return decryptData
  }
  func decodeImage(data: Data) async throws -> Image {
    // decode...
    return avatar
  }
}

比较依据 Callback 的异步版别,依据 async/await 的版别是不是清楚多了。

尤其是 loadAvatar 方法从感观上便是一个同步方法,阅读起来无比顺畅。

其差错处理也运用了同步式的 throws。

至此,通过比照,对 async/await 有了一个较直观的知道,下面简略讨论一下其完成机制。

深究


首要,还是有必要对 async/await 作一个正式的介绍:

  • async — 用于修饰方法,被修饰的方规律被称为异步方法 (asynchronous method),异步方法意味着其在实行过程中或许会被暂停 (挂起);

  • await — 对 asynchronous method 的调用需加上 await。一同,await只能出现在异步上下文中 (asynchronous context);

    await 则标明一个潜在暂停点 (potential suspension points)。

什么是 asynchronous context ?其存在于 2 种环境下:

  • asynchronous method body — 异步方法体归于异步上下文的领域

  • Task closure — Task 任务闭包也归于 asynchronous context。

    Task 是在 Swift 5.5 中引入的,首要用于创立、实行异步任务,后续文章会介绍。

因而,只能在异步方法或 Task 闭包中通过 await 调用异步方法。

异步方法实行过程中或许会暂停?

potential suspension points?

怎样暂停?

刚开端触摸 async/await 时,下意识地或许会有这些疑问。

2 个要害点:

  • 暂停的是方法,而不是实行方法的线程;

  • 暂停点前后或许会发生线程切换。

在 Swift 新并发模型中进一步弱化了『 线程 』,志向情况下整个 App 的线程数应与内核数共同,线程的创立、办理完全交由并发结构担任。

Swift 对异步方法 (asynchronous method) 的处理就恪守了上述思想:

  • 异步方法被暂停点 (suspension points) 分割为若干个 Job

  • 在并发结构中 Job 是任务调度的底子单元;

  • 并发结构依据实时情况动态决议某个 Job 的实行线程;

  • 也便是同一个异步方法中的不同 Job 或许工作在不同线程上。

正是由于异步方法在其暂停点前后或许会变换实行线程,因而在异步方法中要慎用锁、信号量等同步操作。

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()
  }
}

像上面这样的代码在 lock.lock() 处会发生死锁,换成信号量也是相同。

await 之所以称为『 潜在 』暂停点,而不是暂停点,是因为并不是所有的 await 都会暂停,只需遇到类似 IO、手动起子线程等情况时才会暂停其时调用栈的工作。

总归,关于异步方法怎样切分 Job 等细节可以不关心,await 或许会暂停其时方法的工作,并在时机成熟后在其他线程恢复工作是我们需求明确了解的

参考资料

swift-evolution/0296-async-await.md at main apple/swift-evolution GitHub

swift-evolution/0302-concurrent-value-and-concurrent-closures.md at main apple/swift-evolution GitHub

swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main apple/swift-evolution GitHub

swift-evolution/0304-structured-concurrency.md at main apple/swift-evolution GitHub

swift-evolution/0306-actors.md at main apple/swift-evolution GitHub

swift-evolution/0337-support-incremental-migration-to-concurrency-checking.md at main apple/swift-evolution GitHub

Understanding async/await in Swift • Andy Ibanez

Concurrency — The Swift Programming Language (Swift 5.6)

Connecting async/await to other Swift code | Swift by Sundell