如何在不阻塞 UI 更新的情况下使用 Swift 并发在后台执行 CPU 绑定任务?
How to execute a CPU-bound task in background using Swift Concurrency without blocking UI updates?
我有一个 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
}
}
我使用 Task
使用新的并发功能在后台进行计算。这需要使用 @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
}
}
}
}
问题是计算似乎阻塞了整个UI:计数器冻结。
为什么 UI 冻结以及如何以不阻止 UI 更新的方式实施 .compute()
?
我试过的
- 制作
heavyWork
和异步方法并分散 await Task.yield()
每次更新已发布的 属性 似乎可行,但这既麻烦又容易出错。此外,它允许 some UI 更新,但不允许在后续 Task.yield()
调用之间更新。
- 如果我们忽略紫色警告说应该对主要参与者进行 UI 更新(尽管这不是有效的解决方案),则删除
@MainActor
属性似乎有效。
编辑 1
感谢@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)
}
}
问题是 heavyWork
从 Controller
继承了 MainActor
隔离,这意味着工作将在主线程上执行。
这是因为你用 @MainActor
注释了 Controller
所以这个 class 上的所有属性和方法将默认继承 MainActor
隔离。
而且 当您创建一个新的 Task { }
时,它会继承当前任务(即 MainActor
的)当前优先级和 actor 隔离 - 强制 heavyWork
到主要 actor/thread 上的 运行。
我们需要确保 (1) 我们 运行 低优先级的繁重工作,因此系统不太可能将其安排在 UI 线程上。
这也需要是一个分离的任务,这将阻止 Task { }
执行的默认继承。
我们可以通过使用低优先级的 Task.detached
来做到这一点(比如 .background
或 .low
)。
然后(2),我们保证heavyWork
是nonisolated
,所以它不会继承Controller
的@MainActor
上下文。
但是,这确实意味着您不能再直接改变 Controller
上的任何状态。
如果您 await
访问读取操作或 await
调用 actor 上修改状态的其他方法,您仍然可以 read/modify actor 的状态。
在这种情况下,您需要将 heavyWork
设为 async
函数。
然后(3),我们等待使用“任务句柄”编辑的value
属性 return计算值。
这允许我们从 heavyWork
函数访问 return 值,如果有的话。
@MainActor
final class Controller: ObservableObject {
@Published private(set) var isComputing: Bool = false
func compute() {
if isComputing { return }
Task {
isComputing = true
// (1) run detached at a non-UI priority
let work = Task.detached(priority: .low) {
self.heavyWork()
}
// (3) non-blocking wait for value
let result = await work.value
print("result on main thread", result)
isComputing = false
}
}
// (2) will not inherit @MainActor isolation
nonisolated func heavyWork() -> String {
sleep(5)
return "result of heavy work"
}
}
也许我找到了替代方法,使用 actor hopping
。
使用与上面相同的ContentView
,我们可以在标准演员上移动heavyWork
:
@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"
}
}
由于 MainActor
的上下文继承,函数 compute()
在主线程上被调用,但是由于 actor 跳跃,函数 heavyWork()
在主线程上被调用不同线程解锁 UI.
我有一个 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
}
}
我使用 Task
使用新的并发功能在后台进行计算。这需要使用 @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
}
}
}
}
问题是计算似乎阻塞了整个UI:计数器冻结。
为什么 UI 冻结以及如何以不阻止 UI 更新的方式实施 .compute()
?
我试过的
- 制作
heavyWork
和异步方法并分散await Task.yield()
每次更新已发布的 属性 似乎可行,但这既麻烦又容易出错。此外,它允许 some UI 更新,但不允许在后续Task.yield()
调用之间更新。 - 如果我们忽略紫色警告说应该对主要参与者进行 UI 更新(尽管这不是有效的解决方案),则删除
@MainActor
属性似乎有效。
编辑 1
感谢@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)
}
}
问题是 heavyWork
从 Controller
继承了 MainActor
隔离,这意味着工作将在主线程上执行。
这是因为你用 @MainActor
注释了 Controller
所以这个 class 上的所有属性和方法将默认继承 MainActor
隔离。
而且 当您创建一个新的 Task { }
时,它会继承当前任务(即 MainActor
的)当前优先级和 actor 隔离 - 强制 heavyWork
到主要 actor/thread 上的 运行。
我们需要确保 (1) 我们 运行 低优先级的繁重工作,因此系统不太可能将其安排在 UI 线程上。
这也需要是一个分离的任务,这将阻止 Task { }
执行的默认继承。
我们可以通过使用低优先级的 Task.detached
来做到这一点(比如 .background
或 .low
)。
然后(2),我们保证heavyWork
是nonisolated
,所以它不会继承Controller
的@MainActor
上下文。
但是,这确实意味着您不能再直接改变 Controller
上的任何状态。
如果您 await
访问读取操作或 await
调用 actor 上修改状态的其他方法,您仍然可以 read/modify actor 的状态。
在这种情况下,您需要将 heavyWork
设为 async
函数。
然后(3),我们等待使用“任务句柄”编辑的value
属性 return计算值。
这允许我们从 heavyWork
函数访问 return 值,如果有的话。
@MainActor
final class Controller: ObservableObject {
@Published private(set) var isComputing: Bool = false
func compute() {
if isComputing { return }
Task {
isComputing = true
// (1) run detached at a non-UI priority
let work = Task.detached(priority: .low) {
self.heavyWork()
}
// (3) non-blocking wait for value
let result = await work.value
print("result on main thread", result)
isComputing = false
}
}
// (2) will not inherit @MainActor isolation
nonisolated func heavyWork() -> String {
sleep(5)
return "result of heavy work"
}
}
也许我找到了替代方法,使用 actor hopping
。
使用与上面相同的ContentView
,我们可以在标准演员上移动heavyWork
:
@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"
}
}
由于 MainActor
的上下文继承,函数 compute()
在主线程上被调用,但是由于 actor 跳跃,函数 heavyWork()
在主线程上被调用不同线程解锁 UI.