Shu*_*oto 8 asynchronous async-await swift mainactor
我\xe2\x80\x99m 正在学习Swift async、await、 。@MainActor
我想运行一个很长的过程并显示进度。
\nimport 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}\nRun Code Online (Sandbox Code Playgroud)\n当我点击 \xe2\x80\x9cStart Dispatch\xe2\x80\x9d 按钮时,\xe2\x80\x9cCount\xe2\x80\x9d 已更新,但收到警告:
\n\n\n不允许从后台线程发布更改;确保在模型更新时从主线程发布值(通过像 receive(on:) 这样的运算符)。
\n
我以为该类ViewModel是@MainActor,count属性是在Main线程中操作的,但不是。\n我应该使用DispatchQueue.main.async{}update countwhile 吗@MainActor?
当我点击 \xe2\x80\x9cStart Task\xe2\x80\x9d 按钮时,按下按钮直到完成countupAsync()并且不会更新屏幕上的计数。
最好的解决方案是什么?
\nRob*_*Rob 18
你问:
\n\n\n我以为类
\nViewModel是@MainActor,count属性是在Main线程中操作的,但事实并非如此。我应该使用DispatchQueue.main.async {}更新计数吗@MainActor?
DispatchQueue人们应该完全避免使用。尽可能使用新的并发系统。请参阅 WWDC 2021 视频Swift 并发:更新示例应用程序,获取有关从旧DispatchQueue代码过渡到新并发系统的指导。
如果您有遗留代码DispatchQueue.global,则您位于新的合作池执行器之外,并且您不能依赖参与者来解决此问题。您要么必须手动将更新分派回主队列,要么更好的是,使用新的并发系统并完全停用 GCD。
\n\n当我点击 \xe2\x80\x9cStart Task\xe2\x80\x9d 按钮时,按下按钮直到完成
\ncountupAsync(),并且不会更新屏幕上的 \xe2\x80\x9cCount\xe2\x80\x9d 。
是的,因为它在主要参与者上运行,并且您正在使用 阻塞主线程Thread.sleep(forTimeInterval:)。这违反了新并发系统的一个关键规则/假设,即前进应该始终是可能的。请参阅Swift 并发:幕后花絮,其中说道:
\n\n回想一下,使用 Swift,该语言允许我们维护运行时契约,即线程始终能够取得进展。正是基于这个契约,我们构建了一个协作线程池作为 Swift 的默认执行器。当您采用 Swift 并发性时,重要的是要确保继续在代码中维护此约定,以便协作线程池能够以最佳方式运行。
\n
现在讨论是在不安全原语的背景下进行的,但它同样适用于避免阻塞 API(例如Thread.sleep(fortimeInterval:))。
因此,请使用Task.sleep(nanoseconds:),正如文档指出的那样, \xe2\x80\x9cdoesn\xe2\x80\x99t 会阻止底层线程。\xe2\x80\x9d 因此:
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}\nRun Code Online (Sandbox Code Playgroud)\n和
\nButton("Start Task") {\n Task {\n try await viewModel.countUpAsync()\n }\n}\nRun Code Online (Sandbox Code Playgroud)\nasync-实现await避免阻塞 UI。
在这两种情况下,我们应该避免使用旧的 GCD 和ThreadAPI,因为它们可能会违反新并发系统可能做出的假设。坚持使用新的并发 API,并在尝试与旧的阻塞 API 集成时要小心。
你说:
\n\n\n我想运行一个很长的过程并显示进度。
\n
上面我告诉你如何避免使用Thread.sleepAPI 阻塞(通过使用非阻塞Task再现)。但我怀疑您用作sleep\xe2\x80\x9clong 进程\xe2\x80\x9d 的代理。
不用说,您显然也希望让 \xe2\x80\x9clong 进程\xe2\x80\x9d 在新的并发系统中异步运行。该实现的细节将高度依赖于这个 \xe2\x80\x9clong 进程\xe2\x80\x9d 正在做什么。可以取消吗?它会调用其他异步 API 吗?ETC。
\n我建议您尝试一下,如果您无法弄清楚如何在新的并发系统中使其异步,请使用MCVE就该主题发布一个单独的问题。
\n但是,人们可能会从您的示例中推断出您有一些缓慢的同步计算,您希望在计算期间定期更新您的 UI。这似乎是AsyncSequence. (请参阅 WWDC 2021认识 AsyncSequence。)
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}\nRun Code Online (Sandbox Code Playgroud)\n上面我使用了一个分离任务(因为我有一个缓慢的同步计算),但使用AsyncSequence异步获取值流。
有很多不同的方法(这在很大程度上取决于您的 \xe2\x80\x9clong 进程\xe2\x80\x9d 是什么),但希望这说明了一种可能的模式。
\n