我有一个基于文档的应用程序,它使用 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)
        }
    }
}

它似乎做了我需要的任何事情,那就是排队更新文档上的属性,可以等待并返回错误,就像所有事情都发生在主线程上一样.

推荐答案

显然,如果您的任务没有任何await个或其他暂停点,那么您只需要使用一个参与者,而不使用方法async,它会自动按顺序执行这些任务.

但是,如果您真的要连续执行一系列异步任务,那么您只需要让每个任务等待前一个任务.例如.,

actor Foo {
    private var previousTask: Task<(), Error>?

    func add(block: @Sendable @escaping () async throws -> Void) {
        previousTask = Task { [previousTask] in
            let _ = await previousTask?.result

            return try await block()
        }
    }
}

以上有两个微妙的方面:

  1. 我使用[previousTask]个捕获列表来确保获得之前任务的副本.

  2. 我执行新任务,而不是在它之前.

    如果您在创建新任务之前等待,则有race,如果您启动三个任务,则第二个和第三个任务都将等待first个任务,即第三个任务不等待第二个任务.

而且,也许不用说,因为这是在一个参与者内部,所以它避免了分离任务的需要,同时保持主线程空闲.

enter image description here

Swift相关问答推荐

如何在SwiftUI中创建具有圆角顶部和锯齿状底部边缘的自定义Shape,类似于撕破的纸?

是否有一个Kotlin等价的Swift s @ AddendableReport注释'

如何在SWIFT中使用DiscardingTaskGroup获得任务结果

了解SWIFT中的命名VS位置函数调用

在SWIFTUI&39;S视图修改器中使用等待关键字

如何从我的 macOS 应用程序打开 (.log) 文件?

为什么即使 `C` 是一个不是 `Sendable` 的类,Task` 也能工作?

如何打印出此 struct 中的数据?

如何在 Swift 中使用子定义覆盖父类扩展

如何从另一个 swift 文件中调用函数

数组使用偏移量对自身执行 XOR

RealityKit – 无法加载 ARView(发现为零)

Swift中的分段错误

Swift 5.0 编译器无法导入使用 Swift 4.2.1 编译的模块

Swift 中 NSFetchRequest 的多个 NSPredicates?

由于编译器中的内部保护级别,无法访问框架 init 中的公共 struct

显示 UIAlertController 的简单 App Delegate 方法(在 Swift 中)

无法将 NSAttributedString.DocumentAttributeKey 类型的值转换为 .DocumentReadingOptionKey

从 Swift 初始化程序调用方法

滑动侧边栏菜单 IOS 8 Swift