我有一个基于文档的应用程序,它使用 struct 作为其主要数据/模型.由于模型是(NSDocument
的子类)的属性,因此需要从主线程访问它.到目前为止一切都很好.
但是对数据的一些操作可能需要相当长的时间,我想为用户提供一个进度条.这就是问题的起点.尤其是当用户从GUI快速连续启动两个操作时.
如果我在模型上同步运行操作(或在"正常"Task {}
中),我会获得正确的串行行为,但主线程被阻塞,因此我无法显示进度条.(选项A)
如果我在Task.detached {}
闭包中运行模型上的操作,我可以更新进度栏,但取决于模型上操作的运行时间,用户的第二个操作可能在第一个操作之前完成,从而导致模型的无效/意外状态.这是由于分离任务中需要await
条语句(我认为).(选项B).
所以我想a)释放主线程来更新GUI,b)确保每个任务在另一个(排队)任务开始之前运行到完全完成.使用后台串行调度队列是很有可能的,但我正在try 切换到新的Swift并发系统,该系统还用于在访问模型之前执行任何准备工作.
我try 使用全局参与者,因为这似乎是某种串行背景队列,但它也需要await
条语句.虽然模型中出现意外状态的可能性降低了,但仍然有可能.
我编写了一些小代码来演示这个问题:
模型:
struct Model {
var doneA = false
var doneB = false
mutating func updateA() {
Thread.sleep(forTimeInterval: 5)
doneA = true
}
mutating func updateB() {
Thread.sleep(forTimeInterval: 1)
doneB = true
}
}
和文件(不包括标准NSDocument
覆盖):
@globalActor
struct ModelActor {
actor ActorType { }
static let shared: ActorType = ActorType()
}
class Document: NSDocument {
var model = Model() {
didSet {
Swift.print(model)
}
}
func update(model: Model) {
self.model = model
}
@ModelActor
func updateModel(with operation: (Model) -> Model) async {
var model = await self.model
model = operation(model)
await update(model: model)
}
@IBAction func operationA(_ sender: Any?) {
//Option A
// Task {
// Swift.print("Performing some A work...")
// self.model.updateA()
// }
//Option B
// Task.detached {
// Swift.print("Performing some A work...")
// var model = await self.model
// model.updateA()
// await self.update(model: model)
// }
//Option C
Task.detached {
Swift.print("Performing some A work...")
await self.updateModel { model in
var model = model
model.updateA()
return model
}
}
}
@IBAction func operationB(_ sender: Any?) {
//Option A
// Task {
// Swift.print("Performing some B work...")
// self.model.updateB()
// }
//Option B
// Task.detached {
// Swift.print("Performing some B work...")
// var model = await self.model
// model.updateB()
// await self.update(model: model)
// }
//Option C
Task.detached {
Swift.print("Performing some B work...")
await self.updateModel { model in
var model = model
model.updateB()
return model
}
}
}
}
点击"操作A",然后点击"操作B",应该会得到一个有两个true
的模型.但并不总是这样.
有没有办法确保操作a在进入操作B之前完成,并且主线程可用于GUI更新?
EDIT
class Document {
func updateModel(operation: @escaping (Model) throws -> Model) async throws {
//Update the model in the background
let modelTask = Task.detached { [previousTask, model] () throws -> Model in
var model = model
//Check whether we're cancelled
try Task.checkCancellation()
//Check whether we need to wait on earlier task(s)
if let previousTask = previousTask {
//If the preceding task succeeds we use its model
do {
model = try await previousTask.value
} catch {
throw CancellationError()
}
}
return try operation(model)
}
previousTask = modelTask
defer { previousTask = nil } //Make sure a later task can always start if we throw
//Wait for the operation to finish and store the model
do {
self.model = try await modelTask.value
} catch {
if error is CancellationError { return }
else { throw error }
}
}
}
呼叫方:
@IBAction func operationA(_ sender: Any?) {
//Option D
Task {
do {
try await updateModel { model in
var model = model
model.updateA()
return model
}
} catch {
presentError(error)
}
}
}
它似乎做了我需要的任何事情,那就是排队更新文档上的属性,可以等待并返回错误,就像所有事情都发生在主线程上一样.