Modern Concurrency in Swift 的学习笔记

1,372 阅读7分钟

Modern Concurrency in SwiftRaywenderlich 出版的关于 Swift 5.5 异步编程的书籍。

前段时间黑五搞活动半价购买了这本书,现已读完,并把关键知识点整理到我 GitHub 的学习笔记中,有兴趣的读者可以点击查看

有兴趣仔细学习 Swift 5.5 异步编程的,建议购买仔细阅读,非常值得一看。

以下是关于 Swift 5.5 异步编程的一些介绍。

为什么需要现代化的 Swift 并发

苹果上一次大谈异步框架,是在 2009 年 GCD (Grand Central Dispatch) 问世的时候。

虽然 GCD 在 2014 年帮助 Swift 从第一天开始就支持并发和异步,但这种支持不是本地的 —— 它是围绕 Objective-C 的需求和能力设计的。Swift 只是“借用”了这种并发,直到它有了自己的机制,专门为该语言设计。

Swift 5.5 改变了这一切,它为编写异步并发代码引入了一个新的本地模型。新的并发模型提供了在 Swift 中编写安全、高性能程序所需的一切,包括:

  • 用于以结构化方式运行异步操作的新的本地语法。
  • 设计异步和并发代码的标准 API。
  • libdispatch 框架中的底层更改,使所有高级更改直接集成到操作系统中。
  • 为创建安全的并发代码提供了新级别的编译器支持。

Swift 5.5 引入了新的语法和 API 来支持这些功能。在应用程序中,除了使用最新的 Swift 版本外,还需要针对某些平台版本:

  • 如果使用的是 Xcode 13.2 或更高版本,它会将新的并发运行时与应用程序捆绑在一起,这样就可以将 iOS 13 和 macOS 10.15 作为最低的支持版本(对于本地应用程序)。
  • 如果使用的是 Xcode 13,但版本低于 13.2,那么只能把 iOS 15 或 macOS 12(或更高版本)作为最低的支持版本。

理解异步和并发代码

大多数代码的运行方式与在代码编辑器中看到的相同:从上到下,从函数的开头开始,逐行进行到结尾。

这使得确定任何给定的代码行何时执行变得很容易。函数调用也是如此:当代码同步运行时,执行按顺序进行。

在同步的环境中,代码在单个 CPU 内核上的一个线程中运行。你可以想象同步就像一条单行道上的汽车,每辆车都跟在前面的车。即使一辆车有更高的优先级,比如救护车,它也不能“跳过”其他车辆,开得更快。

另一方面,iOS 应用程序和基于 Cocoa 的 macOS 应用程序本质上是异步的。

异步执行允许程序的不同部分在一个线程上以任意顺序运行,有时,根据许多不同的事件(如用户输入、网络连接等)在多个线程上同时运行。

在异步环境中,很难确定函数运行的确切顺序,特别是当多个异步函数需要使用同一线程时。就像在有红绿灯和交通需要让行的道路上驾驶一样,函数有时必须等到轮到它们继续,甚至必须停下来,直到它们得到绿灯才能继续。

异步调用的一个示例是发出网络请求,并提供在 web 服务器响应时运行的闭包。在等待运行完成回调时,应用程序会利用这段时间做其他任务。

为了有意识地并行运行部分程序,可以使用并发 API。一些 API 支持同时执行固定数量的任务;其他 API 支持启动一个并发组并允许任意数量的并发任务。

这也会导致大量与并发相关的问题。例如,程序的不同部分可能会相互阻止执行,或者可能会遇到非常讨厌的数据竞争,其中两个或多个函数同时访问同一个变量,导致应用程序崩溃或意外破坏应用程序的状态。

然而,如果小心使用,并发可以通过在多个 CPU 核上同时执行不同的功能来帮助程序运行得更快,就像小心的驾驶员在多车道高速公路上行驶得更快一样。

在执行代码时,高优先级任务可以在低优先级任务之前“跳转”队列,因此可以避免阻塞主线程,并让它自由地对 UI 进行关键更新。

虽然异步和并发听起来都很棒,但我们可能会问自己:“为什么 Swift 需要一个新的并发模型?”。在过去,我们可能使用过至少部分上述功能的应用程序。

接下来,我们回顾一下 Swift 5.5 之前的并发方法,并了解新的 async/await 模型的不同之处。

回顾以前的并发方法

在Swift 5.5之前,我们使用 GCD 通过调度队列运行异步代码。还使用了更老的 API,如 OperationThread,甚至直接与基于 C 的 pthread 库交互。

这些 API 都使用相同的基础:POSIX 线程,一种不依赖任何编程语言的标准化执行模型。每个执行流都是一个线程,多个线程可能重叠并同时运行。

OperationThread 这样的线程 wrapper 要求我们手动管理执行。也就是要我们自己负责创建和销毁线程,决定并发任务的执行顺序,并跨线程同步共享数据。这是一项容易出错且乏味的工作。

GCD 基于队列的模型运行良好。但是,这通常会导致以下问题:

  • 线程激增:创建太多并发线程需要在活动的线程之间不断切换。这最终会减慢应用程序。
  • 优先级反转:当任意低优先级任务阻止在同一队列中等待的高优先级任务执行时。
  • 缺少执行层次结构:异步代码块缺少执行层次结构,这意味着每个任务都是独立管理的。这使得取消或访问正在运行的任务变得困难。这也使得任务向调用者返回结果变得复杂。

为了解决这些缺点,Swift 引入了一种全新的并发模型。接下来,将看到 Swift 中的现代并发是怎样的!

介绍现代的 Swift 并发模型

新的并发模型与语言语法、Swift 运行时和 Xcode 紧密集成。它为开发人员抽象了线程的概念。其主要新功能包括:

  • 协作线程池。
  • async/await 语法。
  • 结构化并发。
  • 上下文感知代码编译。

1. 协作线程池

新模型透明地管理线程池,以确保它不会超过可用的 CPU 内核数。这样运行时就不需要创建和销毁线程,也不需要经常执行非常耗时的线程切换。相反,代码可以挂起,然后在池中的任何可用线程上快速恢复。

2. async/await 语法

Swift 新的 async/await 语法让编译器和运行时知道一段代码将来可能会暂停并恢复执行一次或多次。运行时可以无缝地为我们处理这些问题,因此不必担心线程和内核的问题。

还有一个好处是,新的语法通常不再需要弱或强捕获 self 或其他变量,因为不需要将escaping closures 用作回调。

3. 结构化并发

现在,每个异步任务都是层次结构的一部分,具有父任务和给定的执行优先级。此层次结构允许运行时在取消父任务时取消所有子任务。此外,它允许运行时在父级完成之前等待所有子级完成。

这种层次结构提供了巨大的优势和更明显的结果,其中高优先级任务将在层次结构中的任何低优先级任务之前运行。

4. 上下文感知代码编译

编译器跟踪给定代码段是否可以异步运行。如果是这样,它就不会让您编写潜在的不安全代码,比如改变共享状态。

这种高水平的编译器意识支持复杂的新功能,如 actors,它区分了在编译时对其状态的同步和异步访问,并通过使编写不安全代码变得更加困难来防止无意中损坏数据。