gpt4 book ai didi

swift - 使用 Swift 异步、等待、@MainActor 的后台任务的最佳解决方案是什么

转载 作者:行者123 更新时间:2023-12-05 08:29:24 26 4
gpt4 key购买 nike

我正在研究 Swift 的asyncawait@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()
}
}

当我点击“Start Dispatch”按钮时,“Count”会更新但会收到警告:

Publishing changes from background threads is not allowed; make sureto publish values from the main thread (via operators likereceive(on:)) on model updates.

我认为 ViewModel 类是 @MainActorcount 属性是在 Main 线程中操作的,但不是.我是否应该使用 DispatchQueue.main.async{} 来更新 count 而不是 @MainActor

当我点击“开始任务”按钮时,按钮会一直按下直到 countupAsync() 完成并且不会在屏幕上更新计数。

什么是最好的解决方案?

最佳答案

你问:

I thought the class ViewModel is @MainActor, count property is manipulated in Main thread, but not. Should I use DispatchQueue.main.async {} to update count although @MainActor?

应该完全避免使用 DispatchQueue。尽可能使用新的并发系统。观看 WWDC 2021 视频 Swift concurrency: Update a sample app有关从旧的 DispatchQueue 代码转换到新的并发系统的指南。

如果您有带有 DispatchQueue.global 的遗留代码,那么您在新的合作池执行器之外,您不能依赖参与者来解决这个问题。您要么必须手动将更新分派(dispatch)回主队列,要么更好地使用新的并发系统并完全淘汰 GCD。

When I tap “Start Task” button, button is pressed until the countupAsync() is done and not update “Count” on screen.

是的,因为它在主要参与者上运行,并且您正在使用 Thread.sleep(forTimeInterval:) 阻塞主线程。这违反了新并发系统的一个关键原则/假设,即前进的进展应该总是可能的。参见 Swift concurrency: Behind the scenes ,它说:

Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.

现在讨论是在不安全原语的上下文中进行的,但它同样适用于避免阻塞 API(例如 Thread.sleep(fortimeInterval:))。

因此,改为使用 Task.sleep(nanoseconds:),即 the docs指出,“不会阻塞底层线程。”因此:

func countUpAsync() async throws {
print("countUpAsync() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
count += 1
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
}
}

Button("Start Task") {
Task {
try await viewModel.countUpAsync()
}
}

async-await 实现避免阻塞 UI。


在这两种情况下,都应该简单地避免使用旧的 GCD 和 Thread API,它们可能会违反新并发系统可能做出的假设。坚持使用新的并发 API,并在尝试与旧的、阻塞的 API 集成时要小心。


你说:

I want to run a long process and display the progress.

上面我告诉您如何避免使用 Thread.sleep API 进行阻塞(通过使用非阻塞 Task 再现)。但我怀疑您使用 sleep 作为“长过程”的代理。

不用说,您显然也希望让您的“长流程”在新的并发系统中异步运行。该实现的细节将高度依赖于这个“漫长的过程”正在做什么。可以取消吗?它会调用其他一些异步 API 吗?等等

我建议您试一试,如果您无法弄清楚如何在新的并发系统中使其异步,请针对该主题发布一个单独的问题,并附上 MCVE。 .

但是,从您的示例中可能会推断您有一些缓慢的同步计算,您希望在计算期间定期更新您的 UI。这似乎是 AsyncSequence 的候选对象。 (参见 WWDC 2021 Meet AsyncSequence。)

func countSequence() async {
let stream = AsyncStream(Int.self) { continuation in
Task.detached {
for _ in 0 ..< 5 {
// do some slow and synchronous calculation here
continuation.yield(1)
}
continuation.finish()
}
}

for await value in stream {
count += value
}
}

上面我使用了一个分离任务(因为我有一个缓慢的同步计算),但使用 AsyncSequence 异步获取值流。

有很多不同的方法(这在很大程度上取决于您的“长过程”是什么),但希望这能说明一种可能的模式。

关于swift - 使用 Swift 异步、等待、@MainActor 的后台任务的最佳解决方案是什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70618542/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com