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

问题描述 投票:0回答:1

我正在学习 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()
完成并且不会更新屏幕上的计数。

最好的解决方案是什么?

swift asynchronous async-await mainactor
1个回答
18
投票

你问:

我以为类

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 中。

© www.soinside.com 2019 - 2024. All rights reserved.