其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记
并发
异步任务对于希望释放资源让系统可以执行其它任务的场景非常有用,比如更新界面,但在希望同步执行两个任务时,就需要用到并发。为此,Swift标准库定义了async let
语句。将异步任务变成多个并发任务,我们只需要使用async let
语句声明处理,如下所示。
示例9-8:定义并发任务
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
let currentTime = Date()
Task(priority: .background) {
async let imageName1 = loadImage(name: "image1")
async let imageName2 = loadImage(name: "image2")
async let imageName3 = loadImage(name: "image3")
let listNames = await "\(imageName1), \(imageName2), \(imageName3)"
print(listNames)
print("Total Time: \(Date().timeIntervalSince(currentTime))")
}
}
}
func loadImage(name: String) async -> String {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
return "Name: \(name)"
}
}
每次完成async let
所声明的处理,系统会创建一个并发任务与其它任务一起并行运行。在示例9-8中,我们创建了三个并发任务(imageName1
、imageName2
和imageName3
)。过程与之前相同,它们调用loadImage()
方法,方向会暂停任务3秒钟,返回一个字符串。但因为这次它们并行运行,完成任务所花费的时间大约为3秒(而不是前例中的9秒)。
✍️跟我一起做:使用示例9-8中的代码更新ContentView
结构体。在模拟器中运行代码。几秒后,会在控制台中打印出处理所耗费的时间。
Actor
在使用并发任务时,可能会碰到数据竞用的问题。数据竞用出现在两个或两个以上并行运行的任务尝试访问相同的数据时。比如,它们同时尝试修改某一个属性的值。这可能会导致错误或严重的bug。为解决这一问题,Swift标准库中引入了actor。
actor是隔离并行任务的数据类型,因此任务在修改actor的值时,另一个任务会强制等待。actor是引用类型,定义类似类,但不是使用class
关键字,而是通过actor
关键字定义。它与类另一个重要的不同是属性和方法必须异步访问(我们必须使用await
关键字等待)。这会确保代码等待actor释放(其它任务不能访问actor)。
下例演示了如何使用actor。这段代码声明了一个一家属性和方法的actor,创建了一个实例,然后在多个任务中调用其中的方法。
示例9-9:定义一个actor
import SwiftUI
actor ItemData {
var counter: Int = 0
func incrementCount() -> String {
counter += 1
return "Value: \(counter)"
}
}
struct ContentView: View {
var item: ItemData = ItemData()
var body: some View {
Button("Start Process") {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
Task(priority: .background) {
async let operation = item.incrementCount()
print(await operation)
}
}
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { (timer) in
Task(priority: .high) {
async let operation = item.incrementCount()
print(await operation)
}
}
}
}
}
界面中的按钮会启动两个无限重复的定时器,一个间隔0.1秒,另一个间隔0.2秒。定时器执行任务并发调用actor中的incrementCount()
方法。这样不同线程中的不同任务会调用该方法,最终会同时调用,产生数据竞用。如果我们将ItemData
声明为类,会报错、出现预期外的行为甚至出现崩溃,但因为我们将这个数据类型声明为actor,代码正确运行。每次在任务调用incrementCount()
方法时,actor会接管并确保一次只有一个任务能访问该方法。
✍️跟我一起做:使用示例9-9中的代码更新ContentView.swift
文件。在iPhone模拟器中运行代码、点击按钮。会看到incrementCount()
方法所产生的值打印在控制台中。停止应用。将actor声明为类(将关键字actor
替换成关键字class
)。这时在方法同时由多个任务调用时会出现错误。
注意:默认Xcode不会在控制台显示异步错误。要监测异步操作的问题,必须激活Thread Sanitizer。点击Xcode工具栏的Scheme按钮(图5-2,2号图)。在菜单中选择Edit Scheme选项(图5-8)。在新窗口中,选择Run选项并打开Diagnostics标签。勾选复选框启用Thread Sanitizer。将actor声明为类,再次在iPhone模拟器中运行应用,点击按钮。在成功运行数次后,会在控制台中看到访问竞争的报错。
我们提到过,actor将属性和方法与其它的代码及线程隔离开。这表示我们只能异步访问actor(必须等待actor允许进行访问),但在某些场景下,不需要进行隔离。这时,我们可以使用如下的关键字反转隔离的状态。
- nonisolated:该关键字打破属性或方法的隔离。
非隔离属性和方法可能遵循协议时要用到,也可以在actor中只需要访问不可变值时简化代码。例如,下例中我们对ItemData
actor添加一个名为maximum
的常量以及一个打印该值的方法。因为常量的值永不改变,我们可以将其声明为