Swift 并发入门

Swift 并发入门

hudson 译 原文

编写异步代码对开发人员来说一直是一项具有挑战性的任务。多年来,苹果提供了各种工具,如GCD(Grand Central Queue )、操作(operations )和调度队列(Dispatch Queue),帮助开发人员编写异步代码。所有这些工具都很棒,但每个工具都有自己的利弊。

在今年的WWDC中,苹果将这一点提升到了一个新的水平,并引入了Swift并发性,这是一种语言内置支持的异步特性,编写简单,易于理解,最重要的是,没有竞争条件。

在本文中,我想快速概述这些技术,async/await , 结构化并发, 以及actor,这是Swift并发的3个主要功能。希望在本文结束时,您将很好地了解什么是Swift并发性,以及如何在自己的项目中开始使用它。

Async/await

那么什么是 async/await ?我们总是听到人们说我们可以使用async/awaits来使我们的代码同时运行,但这不是100%正确。仅使用async/await 不会使您的代码同时运行,它们只是Swift 5.5中引入的关键字,告诉编译器某个代码块应该异步运行。

假设我们有一个执行一些繁重任务的函数,需要一段时间才能完成,我们可以像这样将该函数标记为异步:

swift 复制代码
func performHeavyTask() async {
    
    // Run some heavy tasks here...
}

异步函数是一种特殊类型的函数,可以在执行期间暂停。然而,就像正常函数一样,异步函数也可以返回一个值并抛出错误。

swift 复制代码
func performThrowingHeavyTask() async throws -> String {
    
    // Run some heavy tasks here...
    
    return ""
}

如果一个函数被标记为async,那么我们必须像这样使用await 关键字调用它:

swift 复制代码
await performHeavyTask()

await 关键字表示 performHeavyTask() 函数由于其异步特性可能被暂停。如果我们尝试像正常(同步)函数一样调用 performHeavyTask() 函数,我们会收到一个编译错误,说------不支持并发的函数中的"async"调用。

我们之所以收到这个错误,是因为我们试图在同步上下文中调用异步函数。为了在同步和异步世界之间架起桥梁,我们必须创建一个Task

Task 在Swift 5.5中引入。根据苹果的说法,Task是一个异步工作单元。在任务的上下文中,代码可以暂停并异步运行。因此,我们可以创建一个任务,并用它来调用我们的 performHeavyTask() 函数。方法如下:

swift 复制代码
func doSomething() {

    Task {
        await performHeavyTask()
    }

}

上面的代码将为我们提供类似于使用全局调度队列的行为:

swift 复制代码
func doSomethingElse() {
    
    DispatchQueue.global().async {
        self.performAnotherHeavyTask()
    }
    
}

// Note that this function is not marked as async
func performAnotherHeavyTask() {
    // Run some heavy task here...
}

然而,他们在幕后的工作方式实际上大不相同。

Async/await vs. Dispatch Queue

当我们创建任务时,任务将在任意线程上运行。当线程到达暂停点(代码标记为await等待)时,系统将暂停代码并不再阻塞线程,以便线程可以在等待 performHeavyTask() 完成时继续进行其他工作。一旦执行HeavyTask()完成,任务将重新控制线程,代码将恢复。

就像任务一样,全局调度队列也在任意线程上运行。然而,在等待 performAnotherHeavyTask() 完成时,线程被阻塞。因此,在执行AnotherHeavyTask()返回之前,被阻塞的线程将无法执行任何其他操作。与async/await方法相比,它的效率降低。

下图说明了DispatchQueueTask的程序流程:

模拟长时间运行的任务

如果您想尝试async/await关键字并亲眼看到它们的运行情况,您可以使用Task.sleep(nanoseconds:)方法来模拟长时间运行的任务。这种方法不做任何工作,只是等待给定的纳秒数后再返回。这是一个可等待的方法,因此您可以这样称呼它:

swift 复制代码
func performHeavyTask() async {
    
    // Wait for 5 seconds
    try? await Task.sleep(nanoseconds: 5 * 1_000_000_000)
}

请注意,在调用Task.sleep(_:)方法时,您不需要创建任务,因为 performHeavyTask()被标记为异步,这意味着它将在异步上下文中运行,因此不需要创建任务。

这就是async/await ,接下来我们将看看什么是结构化并发。

结构化并发

假设我们有2个异步函数,返回一个整数值,如下所示:

swift 复制代码
func performTaskA() async -> Int {

    // Wait for 2 seconds
    await Task.sleep(2 * 1_000_000_000)
    return 2
}

func performTaskB() async -> Int {
    
    // Wait for 3 seconds
    await Task.sleep(3 * 1_000_000_000)
    return 3
}

如果我们想得到这两个函数返回的值之和,我们可以这样做:

swift 复制代码
func doSomething() {
    
    Task {
        let a = await performTaskA()
        let b = await performTaskB()
        let sum = a + b
        print(sum) // Output: 5
    }
}

上述代码需要5秒钟才能完成,因为 performTaskA()performTaskB()以串行顺序运行, performTaskA()必须先完成,然后才能启动performTaskB()

正如您现在可能已经注意到的,上面的代码不是最佳的。由于 performTaskA()performTaskB() 彼此独立,我们可以通过同时运行 performTaskA()performTaskB() 来改善执行时间,使其只需 3 秒即可完成。这就是结构化并发发挥作用的地方。

结构化并发的工作原理是,我们将创建2个子任务,同时执行 performTaskA()performTaskB()。在Swift 5.5中,创建子任务有2种主要方法:

  1. 使用async-let绑定
  2. 使用任务组(task group)

对于本文,让我们专注于更直接的方式------使用async-let绑定。

Async-let 绑定

以下代码演示了如何在我们之前的示例中应用async-let :

swift 复制代码
func doSomething() {
    
    Task {
        // Create & start a child task
        async let a = performTaskA()

        // Create & start a child task
        async let b = performTaskB()

        let sum = await (a + b)
        print(sum) // Output: 5
    }
}

在上述代码中,请注意我们如何组合asynclet关键字,在performTaskA()performTaskB()函数上创建async -let绑定。这样做将创建2个同时执行这两个函数的子任务。

由于 performTaskA()performTaskB() 都标记为async,我们需要等待这两个函数完成才能获得 ab 的值。因此,在获取ab的值时,我们必须使用await关键字来指示代码可能会在等待 performTaskA()performTaskB()完成时暂停。

Actor

在处理异步和并发代码时,我们可能遇到的最常见的问题是数据竞争和死锁。这类问题很难调试,也很难修复。随着Swift 5.5中引入Actors技术,我们现在可以依靠编译器在我们的代码中标记任何潜在的竞争条件。那么Actors是如何工作的呢?

Actor如何工作?

Actors 是引用类型,其工作方式与类类似。然而,与类不同,Actors将确保一次只有1个任务可以修改其状态,从而消除竞争条件的根本原因------多个任务同时访问/更改相同的对象状态。

为了创建一个Actor,我们需要使用actor 关键字对它进行注释。以下是一个具有计数功能的actor,它有一个count可变状态,可以使用 addCount() 方法进行修改:

swift 复制代码
actor Counter {
    
    private let name: String
    private var count = 0
    
    init(name: String) {
        self.name = name
    }
    
    func addCount() {
        count += 1
    }
    
    func getName() -> String {
        return name
    }
}

我们可以实例化一个actor,就像实例化一个类一样:

swift 复制代码
// Instantiate a `Counter` actor
let counter = Counter(name: "My Counter")

现在,如果我们尝试在Counter actor之外调用addCount()方法,我们将收到一个编译器错误- Actor-isolated instance method 'addCount()' can not be referenced from a non-isolated context

我们遇到这个错误的原因是编译器试图保护Counter actor的状态。如果假设同时有多个线程调用addCount(),那么将出现竞争条件。因此,我们不能像调用普通实例方法一样简单地调用actor的方法。

要实施此限制,我们必须用await关键字标记调用点,表示addCount()方法在调用时可能会暂停。这实际上很有意义,因为为了维持对count变量的互斥访问,addCount()的调用点可能需要暂停,以便等待其他任务完成后再继续。

考虑到这一点,我们可以应用我们在async/await 部分中学到的内容,并像这样调用addCount()

swift 复制代码
let counter = Counter(name: "My Counter")

Task {
    await counter.addCount()
}

nonisolated 关键词

现在,我想提请您注意CountergetName()方法。就像addCount()方法一样,调用getName()方法也需要用await等待关键字注释。

但是,如果您仔细观察,getName()方法只访问Counter计数器的名称常量,它不会改变计数器的状态,因此不可能创建竞赛条件。

在这种情况下,我们可以通过将其标记为nonisolated ,将getName() 方法排除在actor的保护之外。

swift 复制代码
nonisolated func getName() -> String {
    return name
}

有了这个,我们现在可以像普通实例方法一样调用getName()方法:

swift 复制代码
let counter = Counter(name: "My Counter")
let x = counter.getName()

在结束这篇文章之前,我想谈谈关于actor的最后一点,那就是MainActor

MainActor

MainActor是一种特殊的actor,总是在主线程上运行。在Swift 5.5中,所有UIKit和SwiftUI组件都被标记为MainActor。由于与UI相关的所有组件都是 main actor,当我们想在后台操作完成后更新UI时,我们不再需要担心忘记调度到主线程。

如果您有一个应该始终在主线程上运行的类,您可以使用@MainActor关键字对其进行注释,例如:

swift 复制代码
@MainActor
class MyClass {
    
}

但是,如果您只希望类中的特定函数始终在主线程上运行,您可以使用@MainActor关键字注释该函数:

swift 复制代码
class MyClass {

    @MainActor
    func doSomethingOnMainThread() {
        
    }
}

小结

我对Swift并发感到非常兴奋,我可以预见Swift并发在不久的将来肯定会成为编写异步Swift代码的标准。

我在这篇文章中涵盖的内容只是冰山一角,如果您想了解更多信息,我强烈建议您查看WWDC21的这些视频

感谢您的阅读。👨🏻‍💻

相关推荐
一丝晨光2 天前
继承、Lambda、Objective-C和Swift
开发语言·macos·ios·objective-c·swift·继承·lambda
KWMax2 天前
RxSwift系列(二)操作符
ios·swift·rxswift
Mamong3 天前
Swift并发笔记
开发语言·ios·swift
小溪彼岸3 天前
【iOS小组件】小组件尺寸及类型适配
swiftui·swift
Adam.com3 天前
#Swift :回调地狱 的解决 —— 通过 task/await 来替代 nested mutiple trailing closure 来进行 回调的解耦
开发语言·swift
Anakki3 天前
【Swift官方文档】7.Swift集合类型
运维·服务器·swift
KeithTsui4 天前
集合论(ZFC)之 联合公理(Axiom of Union)注解
开发语言·其他·算法·binder·swift
東三城4 天前
【ios】---swift开发从入门到放弃
ios·swift
文件夹__iOS7 天前
[SwiftUI 开发] @dynamicCallable 与 callAsFunction:将类型实例作为函数调用
ios·swiftui·swift