我正在學習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()完成并且不更新螢屏上的計數。
什么是最好的解決方案?
uj5u.com熱心網友回復:
你問:
我認為類
ViewModel是@MainActor,count屬性是在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異步獲取值流。
有很多不同的方法(很大程度上取決于你的“長期程序”是什么),但希望這說明了一種可能的模式。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/407571.html
標籤:
