I am using a task group to wrap repeated calls to a long running async method. Since I may have many calls to this method that need to happen the hope is that they would run in parallel. At the end of the day I have some regular synchronous code that needs to block until all of these asyncs are called.
What's weird is unlike other examples of task groups I have seen I actually do not need the values from the async throws method. They do actions and write them to disk.
Is there not a way to clean up this code?
let swiftAsyncSemaphore = DispatchSemaphore(value: 0)
let taskGroupTask = Task {
await withThrowingTaskGroup(of: ASRJob.self) { group in
for path in filePathsToWorkOn {
group.addTask {
return try await doHardWork(atPath: path)
}
}
do {
for try await _ in group {
// For now we do not store this information
}
} catch {
// For now we do not store the error
}
}
swiftAsyncSemaphore.signal()
}
swiftAsyncSemaphore.wait()
Most examples I see use a map/combine function at the end on the group but I don't need the data so how can I just await for all of them to finish?
Context on where this code will run: This code is run within the main() block of a Synchronous Operation (NSOperation) that goes within an OperationQueue. So the block calling wait is not in swift async code. The goal is to block the Operations completion until this swift async work is done knowing that it may very well run for a long time.
If you use semaphore.wait() from within an asynchronous context, it will produce this warning:
Instance method 'wait' is unavailable from asynchronous contexts; Await a Task handle instead; this is an error in Swift 6
I assume that you must be waiting for this semaphore outside of a asynchronous context, otherwise it would have produced the above warning.
You cannot use semaphores for dependencies in conjunction between async-await tasks. See Swift concurrency: Behind the scenes:
[Primitives] like semaphores ... are unsafe to use with Swift concurrency. This is because they hide dependency information from the Swift runtime, but introduce a dependency in execution in your code. Since the runtime is unaware of this dependency, it cannot make the right scheduling decisions and resolve them. In particular, do not use primitives that create unstructured tasks and then retroactively introduce a dependency across task boundaries by using a semaphore or an unsafe primitive. Such a code pattern means that a thread can block indefinitely against the semaphore until another thread is able to unblock it. This violates the runtime contract of forward progress for threads.
The pattern in async methods would be to await the result of the task group.
let task = Task {
try await withThrowingTaskGroup(of: ASRJob.self) { group in
for path in filePathsToWorkOn {
group.addTask {
try await doHardWork(atPath: path)
}
}
try await group.waitForAll()
}
}
_ = await task.result
For throwing tasks groups, we would also often waitForAll.
Yes, you can technically wait in an Operation, but even that is an anti-pattern. We would generally write a “concurrent” operation. See discussion of “concurrent operations” in the Operation documentation. Or see Trying to Understand Asynchronous Operation Subclass.