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的这些视频

感谢您的阅读。👨🏻‍💻

相关推荐
Keya20 小时前
lipo 命令行指南
ios·xcode·swift
zhangmeng20 小时前
SwiftUI中如何实现子视图向父视图传递数据?
ios·swiftui·swift
Saafo20 小时前
迁移至 Swift Actors
ios·swift
杂雾无尘2 天前
告别构建错误, iOS 开发架构难题全面解析, 避免 CPU 架构陷阱
ios·swift·客户端
大熊猫侯佩3 天前
探秘 WWDC 25 全新 #Playground 宏:提升 Swift 开发效率的超级神器
xcode·swift·wwdc
移动端小伙伴3 天前
10.推送的扩展能力 — 打造安全的通知体验
swift
移动端小伙伴3 天前
推送的扩展能力 — 打造个性化的通知体验
swift
移动端小伙伴3 天前
远程推送(Remote Push Notification)
swift
移动端小伙伴3 天前
本地通知的精准控制三角:时间、位置、情境
swift
移动端小伙伴3 天前
本地通知内容深度解析 — 打造丰富的通知体验
swift