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

What is the best solution for background task using Swift async, await, @MainActor

我正在学习 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 sure to publish values from the main thread (via operators like receive(on:)) on model updates.

我以为classViewModel@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。尽可能使用新的并发系统。有关从旧 DispatchQueue 代码过渡到新并发系统的指南,请参阅 WWDC 2021 视频 Swift concurrency: Update a sample app

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

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

是的,因为它在主要角色上 运行ning 并且您正在使用 Thread.sleep(forTimeInterval:) 阻塞主线程。这违反了新并发系统的一个关键 precept/presumption,即前进进度应该始终是可能的。请参阅 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 吗?等等

我建议您尝试一下,如果您不知道如何在新的并发系统中使其异步,post关于该主题的单独问题,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 异步获取值流。

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