使用 Swift async、await、@MainActor 的后台任务的最佳解决方案是什么

Shu*_*oto 8 asynchronous async-await swift mainactor

我\xe2\x80\x99m 正在学习Swift asyncawait、 。@MainActor

\n

我想运行一个很长的过程并显示进度。

\n
import SwiftUI\n\n@MainActor\nfinal class ViewModel: ObservableObject {\n    @Published var count = 0\n\n    func countUpAsync() async {\n        print("countUpAsync() isMain=\\(Thread.isMainThread)")\n        for _ in 0..<5 {\n            count += 1\n            Thread.sleep(forTimeInterval: 0.5)\n        }\n    }\n\n    func countUp() {\n        print("countUp() isMain=\\(Thread.isMainThread)")\n        for _ in 0..<5 {\n            self.count += 1\n            Thread.sleep(forTimeInterval: 0.5)\n        }\n    }\n}\n\nstruct ContentView: View {\n    @StateObject private var viewModel = ViewModel()\n\n    var body: some View {\n        VStack {\n            Text("Count=\\(viewModel.count)")\n                .font(.title)\n\n            Button("Start Dispatch") {\n                DispatchQueue.global().async {\n                    viewModel.countUp()\n                }\n            }\n            .padding()\n\n            Button("Start Task") {\n                Task {\n                    await viewModel.countUpAsync()\n                }\n            }\n            .padding()\n        }\n        .padding()\n    }\n}\n\nstruct ContentView_Previews: PreviewProvider {\n    static var previews: some View {\n        ContentView()\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

当我点击 \xe2\x80\x9cStart Dispatch\xe2\x80\x9d 按钮时,\xe2\x80\x9cCount\xe2\x80\x9d 已更新,但收到警告:

\n
\n

不允许从后台线程发布更改;确保在模型更新时从主线程发布值(通过像 receive(on:) 这样的运算符)。

\n
\n

我以为该类ViewModel@MainActorcount属性是在Main线程中操作的,但不是。\n我应该使用DispatchQueue.main.async{}update countwhile 吗@MainActor

\n

当我点击 \xe2\x80\x9cStart Task\xe2\x80\x9d 按钮时,按下按钮直到完成countupAsync()并且不会更新屏幕上的计数。

\n

最好的解决方案是什么?

\n

Rob*_*Rob 18

你问:

\n
\n

我以为类ViewModel@MainActorcount属性是在Main线程中操作的,但事实并非如此。我应该使用DispatchQueue.main.async {}更新计数吗@MainActor

\n
\n

DispatchQueue人们应该完全避免使用。尽可能使用新的并发系统。请参阅 WWDC 2021 视频Swift 并发:更新示例应用程序,获取有关从旧DispatchQueue代码过渡到新并发系统的指导。

\n

如果您有遗留代码DispatchQueue.global,则您位于新的合作池执行器之外,并且您不能依赖参与者来解决此问题。您要么必须手动将更新分派回主队列,要么更好的是,使用新的并发系统并完全停用 GCD。

\n
\n

当我点击 \xe2\x80\x9cStart Task\xe2\x80\x9d 按钮时,按下按钮直到完成countupAsync(),并且不会更新屏幕上的 \xe2\x80\x9cCount\xe2\x80\x9d 。

\n
\n

是的,因为它在主要参与者上运行,并且您正在使用 阻塞主线程Thread.sleep(forTimeInterval:)。这违反了新并发系统的一个关键规则/假设,即前进应该始终是可能的。请参阅Swift 并发:幕后花絮,其中说道:

\n
\n

回想一下,使用 Swift,该语言允许我们维护运行时契约,即线程始终能够取得进展。正是基于这个契约,我们构建了一个协作线程池作为 Swift 的默认执行器。当您采用 Swift 并发性时,重要的是要确保继续在代码中维护此约定,以便协作线程池能够以最佳方式运行。

\n
\n

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

\n

因此,请使用Task.sleep(nanoseconds:),正如文档指出的那样, \xe2\x80\x9cdoesn\xe2\x80\x99t 会阻止底层线程。\xe2\x80\x9d 因此:

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

\n
Button("Start Task") {\n    Task {\n        try await viewModel.countUpAsync()\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

async-实现await避免阻塞 UI。

\n
\n

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

\n
\n

你说:

\n
\n

我想运行一个很长的过程并显示进度。

\n
\n

上面我告诉你如何避免使用Thread.sleepAPI 阻塞(通过使用非阻塞Task再现)。但我怀疑您用作sleep\xe2\x80\x9clong 进程\xe2\x80\x9d 的代理。

\n

不用说,您显然也希望让 \xe2\x80\x9clong 进程\xe2\x80\x9d 在新的并发系统中异步运行。该实现的细节将高度依赖于这个 \xe2\x80\x9clong 进程\xe2\x80\x9d 正在做什么。可以取消吗?它会调用其他异步 API 吗?ETC。

\n

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

\n

但是,人们可能会从您的示例中推断出您有一些缓慢的同步计算,您希望在计算期间定期更新您的 UI。这似乎是AsyncSequence. (请参阅 WWDC 2021认识 AsyncSequence。)

\n
func countSequence() async {\n    let stream = AsyncStream(Int.self) { continuation in\n        Task.detached {\n            for _ in 0 ..< 5 {\n                // do some slow and synchronous calculation here\n                continuation.yield(1)\n            }\n            continuation.finish()\n        }\n    }\n\n    for await value in stream {\n        count += value\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

有很多不同的方法(这在很大程度上取决于您的 \xe2\x80\x9clong 进程\xe2\x80\x9d 是什么),但希望这说明了一种可能的模式。

\n