Lou*_*Lac 10 swift swiftui swift-concurrency
我有一个ObservableObject可以完成 CPU 密集型繁重工作的程序:
import Foundation
import SwiftUI
@MainActor
final class Controller: ObservableObject {
@Published private(set) var isComputing: Bool = false
func compute() {
if isComputing { return }
Task {
heavyWork()
}
}
func heavyWork() {
isComputing = true
sleep(5)
isComputing = false
}
}
Run Code Online (Sandbox Code Playgroud)
我使用 aTask在后台使用新的并发功能进行计算。这需要使用该@MainActor属性来确保所有 UI 更新(此处与该属性相关isComputing)都在主要参与者上执行。
然后,我有以下视图,其中显示一个计数器和一个启动计算的按钮:
struct ContentView: View {
@StateObject private var controller: Controller
@State private var counter: Int = 0
init() {
_controller = StateObject(wrappedValue: Controller())
}
var body: some View {
VStack {
Text("Timer: \(counter)")
Button(controller.isComputing ? "Computing..." : "Compute") {
controller.compute()
}
.disabled(controller.isComputing)
}
.frame(width: 300, height: 200)
.task {
for _ in 0... {
try? await Task.sleep(nanoseconds: 1_000_000_000)
counter += 1
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
问题是计算似乎阻塞了整个 UI:计数器冻结。
为什么 UI 会冻结以及如何以.compute()不阻止 UI 更新的方式实现?
heavyWork和异步方法以及分散似乎可行,但这既麻烦又容易出错。await Task.yield()此外,它允许一些UI 更新,但不允许在后续Task.yield()调用之间更新。@MainActor如果我们忽略紫色警告,说明应在主要参与者上进行 UI 更新(但这不是有效的解决方案),则删除该属性似乎可行。感谢@Bradley提出的答案,我得到了这个解决方案,它按预期工作(并且非常接近通常的DispatchQueue方式):
@MainActor
final class Controller: ObservableObject {
@Published private(set) var isComputing: Bool = false
func compute() {
if isComputing { return }
Task.detached {
await MainActor.run {
self.isComputing = true
}
await self.heavyWork()
await MainActor.run {
self.isComputing = false
}
}
}
nonisolated func heavyWork() async {
sleep(5)
}
}
Run Code Online (Sandbox Code Playgroud)
问题是heavyWork继承了MainActor的隔离性Controller,这意味着工作将在主线程上执行。\n这是因为您已使用 进行注释,因此Controller默认@MainActor情况下,该类上的所有属性和方法都将继承隔离MainActor性。\ n当您创建一个新的 时Task { },它将继承当前任务(即该MainActor任务)的当前优先级和执行者隔离\xe2\x80\x93,强制heavyWork在主执行者/线程上运行。
我们需要确保 (1) 以较低的优先级运行繁重的工作,因此系统不太可能将其调度到 UI 线程上。\n这也需要是一个分离任务,这将阻止执行的默认Task { }继承.\n我们可以通过使用Task.detached低优先级(如.background或)来做到这一点.low。
然后 (2),我们确保 is heavyWork,nonisolated因此它不会@MainActor从 继承上下文Controller。\n但是,这确实意味着您不能再Controller直接改变 上的任何状态。\n您仍然可以读取/修改 的状态如果您await访问读取操作或await调用 Actor 上修改状态的其他方法,则为 Actor。\n在这种情况下,您需要创建一个heavyWork函数async。
然后 (3),我们等待使用value“任务句柄”返回的属性计算值。\n这允许我们访问函数的返回值heavyWork(如果有)。
@MainActor\nfinal class Controller: ObservableObject {\n @Published private(set) var isComputing: Bool = false\n \n func compute() {\n if isComputing { return }\n Task {\n isComputing = true\n\n // (1) run detached at a non-UI priority\n let work = Task.detached(priority: .low) {\n self.heavyWork()\n }\n\n // (3) non-blocking wait for value\n let result = await work.value\n print("result on main thread", result)\n\n isComputing = false\n }\n }\n \n // (2) will not inherit @MainActor isolation\n nonisolated func heavyWork() -> String {\n sleep(5)\n return "result of heavy work"\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n
也许我已经找到了替代方案,使用actor hopping.
使用ContentView与上面相同的方法,我们可以将 移动heavyWork到标准 actor 上:
@MainActor
final class Controller: ObservableObject {
@Published private(set) var isComputing: Bool = false
func compute() {
if isComputing { return }
Task {
isComputing = true
print("Controller isMainThread: \(Thread.isMainThread)")
let result = await Worker().heavyWork()
print("result on main thread", result)
isComputing = false
}
}
}
actor Worker {
func heavyWork() -> String {
print("Worker isMainThread: \(Thread.isMainThread)")
sleep(5)
return "result of heavy work"
}
}
Run Code Online (Sandbox Code Playgroud)
由于 的上下文继承,该函数compute()在主线程上被调用MainActor,但随后,由于 actor 跳跃,该函数heavyWork()在另一个线程上被调用,从而解除了 UI 的阻塞。
| 归档时间: |
|
| 查看次数: |
1253 次 |
| 最近记录: |