我正在学习 Swift 的
async
、await
、@MainActor
。
我想运行一个很长的进程并显示进度。
import SwiftUI
@MainActor
final class ViewModel: ObservableObject {
@Published var count = 0
func countUpAsync() async {
print("countUpAsync() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
count += 1
Thread.sleep(forTimeInterval: 0.5)
}
}
func countUp() {
print("countUp() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
self.count += 1
Thread.sleep(forTimeInterval: 0.5)
}
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text("Count=\(viewModel.count)")
.font(.title)
Button("Start Dispatch") {
DispatchQueue.global().async {
viewModel.countUp()
}
}
.padding()
Button("Start Task") {
Task {
await viewModel.countUpAsync()
}
}
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
当我点击“开始调度”按钮时,“计数”会更新,但会收到警告:
不允许从后台线程发布更改;确保 从主线程发布值(通过像 在模型更新时接收(on:))。
我以为类
ViewModel
是@MainActor
,count
属性是在Main
线程中操作的,但不是。
尽管 DispatchQueue.main.async{}
我应该使用 count
来更新 @MainActor
吗?
当我点击“开始任务”按钮时,按下按钮直到
countupAsync()
完成并且不会更新屏幕上的计数。
最好的解决方案是什么?
你问:
我以为类
是ViewModel
,@MainActor
属性是在count
线程中操作的,但不是。尽管Main
我应该使用DispatchQueue.main.async {}
来更新计数吗?@MainActor
尽可能避免使用
DispatchQueue
。保留在新的 Swift 并发系统内。请参阅 WWDC 2021 视频 Swift 并发:更新示例应用程序,获取有关从旧 DispatchQueue
代码过渡到新并发系统的指导。
如果您有带有
DispatchQueue.global
的遗留代码,则您位于新的合作池执行器之外,并且您不能依赖参与者来解决此问题。您要么必须手动将更新分派回主队列,要么更好的是,使用新的并发系统并完全退休 GCD。
当我点击“开始任务”按钮时,按钮会被按下,直到
完成,并且不会更新屏幕上的“计数”。countupAsync()
是的,因为它在主要参与者上运行,并且您正在使用
Thread.sleep(forTimeInterval:)
阻塞主线程。这违反了新并发系统的一个关键规则/假设,即前进应该始终是可能的。请参阅Swift 并发:幕后,其中说道:
回想一下,使用 Swift,该语言允许我们维护运行时契约,即线程始终能够取得进展。正是基于这个契约,我们构建了一个协作线程池作为 Swift 的默认执行器。当您采用 Swift 并发性时,确保您继续在代码中维护此契约非常重要,以便协作线程池能够以最佳方式运行。
现在讨论是在不安全原语的背景下进行的,但它同样适用于避免阻塞 API(例如
Thread.sleep(fortimeInterval:)
)。
因此,请使用
Task.sleep(for:)
,正如文档指出的那样,“不会阻塞底层线程”。因此:
func countUpAsync() async throws {
for _ in 0..<5 {
count += 1
try await Task.sleep(for: .milliseconds(500))
}
}
和
Button("Start Task") {
Task {
try await viewModel.countUpAsync()
}
}
async
-await
实现避免阻塞 UI。
现在,如果“睡眠”调用只是某些
async
函数的占位符,那么只需将 Task.sleep
替换为您正在等待的任何内容即可。
func countUpAsync() async throws {
for i in 0..<5 {
count = i
try await …
}
}
在可能的情况下,应该避免使用旧的 GCD 和
Thread
API,这可能违反新并发系统可能做出的假设。坚持使用 Swift 并发。
“不使用 GCD”的一个例外是循环中的任务正在执行任何同步操作。我们与 Swift 并发有一个约定,永远不会阻塞协作线程池中的线程。有关更多信息,请参阅针对此边缘情况场景的将阻塞函数集成到 Swift async 中。