I am learning Swift's async/await and I would like to write a function that would expect several calls to happen concurrently (not sequentially), and every time one of the calls is finished, get a progress completion. I tried this:
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()
}
}
Called like this:
print("Go [\(Date())]")
Task {
await load { progress in
print("Progress [\(Date())]: \(progress)")
}
print("Done!")
}
I expect this code to start the three calls at the same time, executing concurrently, the third one taking 2 seconds should finish first, then the first one taking 4 seconds and the second one taking 7 seconds. Also, every time a call is finished, I want the progress completion block to be called.
But here is the output:
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!
Obviously the progress lines aren't getting printed when I expect in the console. Why is that?
Thank you for your help
You are running your tasks concurrently, but awaiting the results sequentially. Consider:
for task in tasks {
await task.value
…
}
Even though the tasks run concurrently, the above will await their respective values sequentially, one after another. I.e., it will not even reach the await of the second task until the first one yielded a value. If you use a “task group”, it will let you await them in whatever order they complete.
So, a few details:
You should avoid unnecessary unstructured concurrency (either Task {…} or Task.detached {…}) wherever possible. If we remain within structured concurrency, we enjoy automatic handling of task cancelation.
Rather than keeping your own array of tasks, use a “task group” (e.g., withTaskGroup or withThrowingTaskGroup). Then you can await the group, and the tasks can finish in whatever order they want.
Note, when you use task groups, we now need to worry about the thread-safety of the counters. So, you might create an actor to keep track of the progress:
actor CallProgress {
var total = 0
var count = 0
var fractionCompleted: Double { Double(count) / Double(total) }
func add() {
total += 1
}
func finish() {
count += 1
}
}
And then use it like so:
func load(progressHandler: @Sendable @escaping (Double) -> Void) async {
let calls: [Call] = [.first, .second, .third]
let progress = CallProgress()
await withTaskGroup(of: Void.self) { group in
for call in calls {
await progress.add()
group.addTask { [self, progress] in
await testFunc(call)
await progress.finish()
await progressHandler(progress.fractionCompleted)
}
}
}
}
Or you could use Progress object.
func load(progressHandler: @Sendable @escaping (Double) -> Void) async {
let calls: [Call] = [.first, .second, .third]
let progress = Progress()
await withTaskGroup(of: Void.self) { group in
for call in calls {
progress.totalUnitCount += 1
group.addTask { [self, progress] in
await testFunc(call)
progress.completedUnitCount += 1
progressHandler(progress.fractionCompleted)
}
}
}
}
Note, Progress offers some pretty rich capabilities (you can rollup a tree of objects; its fractionCompleted is observable; integration with UI types like UIProgressView; etc.), but I am just taking advantage of a thread-safe object that wraps completedUnitCount and totalUnitCount.
Also note that the closure should be Sendable. You might have to set “strict concurrency checking” build setting to “complete” to see all these thread-safety issues.
Alternatively, rather than a closure, we might use an AsyncSequence for progress updates. E.g.:
func loadUpdates() async -> AsyncStream<Double> {
let calls: [Call] = [.first, .second, .third]
let progress = Progress()
return AsyncStream { continuation in
let task = Task {
await withTaskGroup(of: Void.self) { group in
for call in calls {
progress.totalUnitCount += 1
group.addTask { [self, progress] in
await testFunc(call)
progress.completedUnitCount += 1
continuation.yield(progress.fractionCompleted)
}
}
await group.waitForAll()
continuation.finish()
}
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
And then:
func start() async {
print("Go [\(Date())]")
for await progress in await loadUpdates() {
print("Progress [\(Date())]: \(progress)")
}
print("Done!")
}
There are lots of other patterns, too. For example, in UIKit we might use a return a Progress and just set the observedProgress of the UIProgressView. Or in SwiftUI, if this was an ObservableObject, we might just update a @Published “fraction completed” value ourselves. There is not enough context in the question to be more specific.
The bottom line is that we might tend to favor other patterns over closures and we would use a task group to keep track of our tasks.
As you have been told in a comment, you are describing (but not using) a task group (or other async stream), whose individual task results you can receive with for await. And as you have also been told in a comment, you need to stop trying to "mix-and-match" async/await code with other technologies such as dispatch (asyncAfter) and think purely in async/await terms. Any basic WWDC video on async/await will teach you about both of those things, so I suggest you try watching one.
Meanwhile, here is code that does what I believe you are trying to do:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task { await start() }
}
enum Call: String {
case first = "first"
case second = "second"
case third = "third"
var delay: TimeInterval {
switch self {
case .first: return 4
case .second: return 7
case .third: return 2
}
}
}
func callcall(_ call: Call) async -> String {
try? await Task.sleep(for: .seconds(call.delay))
return call.rawValue
}
func start() async {
let calls: [Call] = [.first, .second, .third]
await withTaskGroup(of: String.self) { group in
calls.forEach { call in
print(Date(), "starting", call.rawValue)
group.addTask {
await self.callcall(call)
}
}
for await result in group {
print(Date(), "finishing", result)
}
print("done")
}
}
}
Output:
2023-05-03 13:13:54 +0000 starting first
2023-05-03 13:13:54 +0000 starting second
2023-05-03 13:13:54 +0000 starting third
2023-05-03 13:13:56 +0000 finishing third
2023-05-03 13:13:58 +0000 finishing first
2023-05-03 13:14:01 +0000 finishing second
done
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With