0

我正在学习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@MainActorcount属性是在Main线程中操作的,但不是。我应该使用DispatchQueue.main.async{}更新count@MainActor

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

什么是最好的解决方案?

4

1 回答 1

3

您询问:

我认为类ViewModel@MainActorcount属性是在Main线程中操作的,但不是。我应该使用DispatchQueue.main.async {}更新计数@MainActor吗?

一个人应该避免使用DispatchQueue。尽可能使用新的并发系统。请参阅 WWDC 2021 视频Swift 并发:更新示例应用程序以获取有关从旧DispatchQueue代码转换到新并发系统的指导。

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

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

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

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

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

因此,请改为使用Task.sleep(nanoseconds:),正如文档指出的那样,“不会阻塞底层线程”。因此:

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 和ThreadAPI,这可能会违反新并发系统可能做出的假设。坚持使用新的并发 API,并在尝试与旧的阻塞 API 集成时要小心。


你说:

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

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

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

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

但是,您可能会从您的示例中推断出您有一些缓慢的同步计算,您希望在计算期间定期更新您的 UI。这似乎是AsyncSequence. (请参阅 WWDC 2021与 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异步获取值流。

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

于 2022-01-09T20:44:00.823 回答