我正在学习
async/await
和任务。所以我了解到任务基本上继承了演员。
想象我有一个模型:
class SomeModel: ObservableObject {
@Published var downloads: [Int] = []
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
downloads.append(data) // after request
}
}
我有一个简单的视图
struct DownloadView: View {
@EnvironmentObject var model: SomeModel
var body: some View {
List {
// USE model.downloads
}
.task {
do {
try await model.doSome()
} catch {}
}
}
我收到一个错误,提示我正在从后台线程更新 UI。没关系。 然后我添加
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
await MainActor.run {
downloads.append(data)
} // after request
}
我仍然有错误。系统不知道第一个追加总是主角?或者这是来自傻瓜的安全措施,如果某些人在第一次追加之前有一些暂停的代码?
我如何理解系统需要分层等待才能知道哪些子作业树需要暂停。但是暂停点是在第一次追加之后,所以它不能传递给不同的演员。
重现代码: 型号
struct DownloadFile: Identifiable {
var id: String { return name }
let name: String
let size: Int
let date: Date
static let mockFiles = [DownloadFile(name: "File1", size: 100, date: Date()),
DownloadFile(name: "File2", size: 100, date: Date()),
DownloadFile(name: "File3", size: 100, date: Date()),
DownloadFile(name: "File4", size: 100, date: Date()),
DownloadFile(name: "File5", size: 100, date: Date()),
DownloadFile(name: "FIle6", size: 100, date: Date())]
static let empty = DownloadFile(name: "", size: 0, date: Date())
}
初见
struct ContentView: View {
@State var files: [DownloadFile] = []
let model: ViewModel
@State var selected = DownloadFile.empty {
didSet {
isDisplayingDownload = true
}
}
@State var isDisplayingDownload = false
var body: some View {
NavigationStack {
VStack {
// The list of files available for download.
List {
Section(content: {
if files.isEmpty {
ProgressView().padding()
}
ForEach(files) { file in
Button(action: {
selected = file
}, label: {
Text(file.name)
})
}
})
}
.listStyle(.insetGrouped)
}
.task {
try? await Task.sleep(for: .seconds(1))
files = DownloadFile.mockFiles
}
.navigationDestination(isPresented: $isDisplayingDownload) {
DownloadView(file: selected).environmentObject(model)
}
}
}
}
第二个视图
struct DownloadView: View {
let file: DownloadFile
@EnvironmentObject var model: ViewModel
@State var result: String = ""
var body: some View {
VStack {
Text(file.name)
if model.downloads.isEmpty {
Button {
Task {
result = try await model.download(file: file)
}
} label: {
Text("-- Download --")
}
}
Text(result)
}
}
}
视图模型
class ViewModel: ObservableObject {
@Published var downloads: [String] = []
func download(file: DownloadFile) async throws -> String {
downloads.append(file.name)
try await Task.sleep(for: .seconds(2))
return "Download Finished"
}
}
系统不知道第一个追加总是主角?
“系统”(或更准确地说是编译器)此时应该如何知道这一点? Swift 并发中没有魔法可以识别,如果您从特定参与者访问一个代码位置中的属性,则对该属性的所有访问都应受到该参与者的保护。
在您的情况下,
SomeModel
未绑定到特定参与者,doSome
函数也未绑定到特定演员。
任务修饰符创建一个新的顶级任务,如果您不将其绑定到特定参与者,则会遇到所描述的问题。
子任务继承其所属的顶级任务的参与者,但对于顶级任务,您必须专门指定一个参与者。
所以你实际上应该做的是将你的视图模型绑定到主要参与者:
@MainActor
final class SomeModel: ObservableObject {
// ...
}
这意味着对所有属性的访问都受到
MainActor
的保护,并且任何从其他参与者访问属性的尝试都必须作为 async
调用进行。然后,编译器会强制执行此操作,并明确此时必须进行特殊的受保护调用。
粗略地说,此时就好像您必须等待锁,不同之处在于调用线程不会因此被阻塞。
或者,如果您确定自己在做什么,只需将
doSome
方法和 downloads
属性绑定到 MainActor
并确保对 SomeModel
的所有其他访问都受到相应保护:
final class SomeModel: ObservableObject {
@MainActor @Published private(set) var downloads: [Int] = []
// ...
@MainActor
func doSome() async throw {
// ...
}
}
乍一看,下面列出的实现具有相当明显的并发问题:
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
await MainActor.run {
downloads.append(data)
} // after request
}
由于该方法的类未绑定到任何参与者,因此异步
doSome
函数几乎可以由任何任务(因此线程)调用。
这意味着该方法也可以同时调用多次,并且在第一行中您已经对
downloads
进行了并发访问,这是一个问题,因为此时没有任何东西可以保护 downloads
。
虽然您对
downloads
的第二次访问是在 MainActor
上进行的,但其他任务仍可能再次并发调用 doSome
并在此方法的第一行中访问 downloads
。
我通常建议激活“严格并发检查”选项,这将在编译时检测到大量并发问题并发出错误。
此模式也可以使用 Swift 5 激活,但不会显示与 Swift 6 模式一样多的可能错误。