Joh*_*Doe 1 concurrency asynchronous async-await swift
我正在学习 Swift 的 async/await,我想编写一个函数,该函数期望多个调用同时发生(而不是顺序),并且每次一个调用完成时,都会获得进度完成。我试过这个:
enum Call: String {
case first = "first"
case second = "second"
case third = "third"
var delay: TimeInterval {
switch self {
case .first: return 4.0
case .second: return 7.0
case .third: return 2.0
}
}
}
func load(progress: @escaping (Double) -> Void) async {
let calls = [Call.first, .second, .third]
var tasks: [Task<Void, Never>] = []
for call in calls {
tasks.append(Task.detached { [weak self] in
guard let self else { return }
await self.testFunc(call)
})
}
var count = 0
for task in tasks {
await task.value
count += 1
progress(Double(count) / Double(calls.count))
}
return
}
func testFunc(_ call: Call) async {
print("Start call \(call.rawValue) [\(Date())]")
return await withCheckedContinuation { continuation in
delay(call.delay) {
print("End call \(call.rawValue) [\(Date())]")
continuation.resume()
}
}
}
func delay(_ seconds: Double, completion: @escaping () -> Void) {
let popTime = DispatchTime.now() + Double(Int64(Double(NSEC_PER_SEC) * seconds)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: popTime) {
completion()
}
}
Run Code Online (Sandbox Code Playgroud)
像这样调用:
print("Go [\(Date())]")
Task {
await load { progress in
print("Progress [\(Date())]: \(progress)")
}
print("Done!")
}
Run Code Online (Sandbox Code Playgroud)
我希望这段代码同时启动三个调用,并发执行,第三个调用需要 2 秒,应该首先完成,然后第一个调用需要 4 秒,第二个调用需要 7 秒。另外,每次调用完成时,我希望调用进度完成块。
但这里是输出:
Go [2023-05-03 11:10:34 +0000]
Start call first [2023-05-03 11:10:34 +0000]
Start call second [2023-05-03 11:10:34 +0000]
Start call third [2023-05-03 11:10:34 +0000]
End call third [2023-05-03 11:10:36 +0000]
End call first [2023-05-03 11:10:38 +0000]
Progress [2023-05-03 11:10:38 +0000]: 0.3333333333333333
End call second [2023-05-03 11:10:41 +0000]
Progress [2023-05-03 11:10:41 +0000]: 0.6666666666666666
Progress [2023-05-03 11:10:41 +0000]: 1.0
Done!
Run Code Online (Sandbox Code Playgroud)
显然,当我在控制台中期望时,进度线没有被打印。这是为什么?
感谢您的帮助
您正在同时运行任务,但按顺序等待结果。考虑:
\nfor task in tasks {\n await task.value\n \xe2\x80\xa6\n}\nRun Code Online (Sandbox Code Playgroud)\n即使任务同时运行,上面的任务也会await按顺序依次运行各自的值。await即,在第一个任务产生值之前,它甚至不会到达第二个任务的 。如果您使用\xe2\x80\x9c任务组\xe2\x80\x9d,它将让您await按照它们完成的顺序进行操作。
那么,一些细节:
\n您应该尽可能避免不必要的非结构化并发( 或Task {\xe2\x80\xa6}) 。Task.detached {\xe2\x80\xa6}如果我们保持结构化并发,我们就会享受任务取消的自动处理。
不要保留自己的任务数组,而是使用 \xe2\x80\x9ctask group\xe2\x80\x9d (例如withTaskGroup或withThrowingTaskGroup)。然后你可以await分组,任务可以按他们想要的任何顺序完成。
请注意,当您使用任务组时,我们现在需要担心计数器的线程安全性。因此,您可以创建一个参与者来跟踪进度:
\nactor CallProgress {\n var total = 0\n var count = 0\n var fractionCompleted: Double { Double(count) / Double(total) }\n\n func add() {\n total += 1\n }\n\n func finish() {\n count += 1\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n然后像这样使用它:
\nfunc load(progressHandler: @Sendable @escaping (Double) -> Void) async {\n let calls: [Call] = [.first, .second, .third]\n\n let progress = CallProgress()\n\n await withTaskGroup(of: Void.self) { group in\n for call in calls {\n await progress.add()\n\n group.addTask { [self, progress] in\n await testFunc(call)\n await progress.finish()\n await progressHandler(progress.fractionCompleted)\n }\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n或者你可以使用Progress对象。
func load(progressHandler: @Sendable @escaping (Double) -> Void) async {\n let calls: [Call] = [.first, .second, .third]\n\n let progress = Progress()\n\n await withTaskGroup(of: Void.self) { group in\n for call in calls {\n progress.totalUnitCount += 1\n\n group.addTask { [self, progress] in\n await testFunc(call)\n progress.completedUnitCount += 1\n progressHandler(progress.fractionCompleted)\n }\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n请注意,Progress提供了一些相当丰富的功能(您可以汇总对象树;它fractionCompleted是可观察的;与 UI 类型集成UIProgressView;等等),但我只是利用包装completedUnitCount和的线程安全对象totalUnitCount。
还要注意的是,应该关闭Sendable。您可能必须将 \xe2\x80\x9cstrict 并发检查\xe2\x80\x9d 构建设置设置为 \xe2\x80\x9ccomplete\xe2\x80\x9d 才能查看所有这些线程安全问题。
或者,我们可以使用 来更新进度,而不是使用闭包AsyncSequence。例如:
func loadUpdates() async -> AsyncStream<Double> {\n let calls: [Call] = [.first, .second, .third]\n\n let progress = Progress()\n\n return AsyncStream { continuation in\n let task = Task {\n await withTaskGroup(of: Void.self) { group in\n for call in calls {\n progress.totalUnitCount += 1\n group.addTask { [self, progress] in\n await testFunc(call)\n progress.completedUnitCount += 1\n continuation.yield(progress.fractionCompleted)\n }\n }\n await group.waitForAll()\n continuation.finish()\n }\n }\n\n continuation.onTermination = { _ in\n task.cancel()\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n进而:
\nfunc start() async {\n print("Go [\\(Date())]")\n\n for await progress in await loadUpdates() {\n print("Progress [\\(Date())]: \\(progress)")\n }\n\n print("Done!")\n}\nRun Code Online (Sandbox Code Playgroud)\n还有很多其他模式。例如,在 UIKit 中,我们可能会使用 return aProgress并仅设置observedProgress的UIProgressView。或者在 SwiftUI 中,如果这是一个ObservableObject,我们可能会自己更新@Published\xe2\x80\x9cfractioncompleted\xe2\x80\x9d 值。问题中没有足够的上下文来更具体。
最重要的是,我们可能倾向于选择其他模式而不是闭包,并且我们会使用任务组来跟踪我们的任务。
\n