In my TestApp I run the following code, to calculate every pixel of a bitmap concurrently:
private func generate() async {
for x in 0 ..< bitmap.width{
for y in 0 ..< bitmap.height{
let result = await Task.detached(priority:.userInitiated){
return iterate(x,y)
}.value
displayResult(result)
}
}
}
This works and does not give any warnings or runtime issues.
The number of active tasks continuously raises until 2740 and stays constant at this value even after all 64000 pixels have been calculated and displayed.
Is this a problem with my code, Instruments or Swift?
Could you fix this problem? My problem is similar:
When I run my app on a simulator, the number of alive tasks varies a little, but remains essentially constant. When I run the same app on a device, the number of alive tasks increases steadily. I don't know if this is a problem of my code (I don't think so), or if there is a bug in Instruments or iOS.
If you are seeing “alive tasks” grow without ever dropping with the code provided in your question, this could be a false positive from Instruments. I haven't experienced this behavior with the “Swift Tasks” tool, but I have seen all sorts of weird behaviors in Instruments, so it is not entirely surprising if you found an edge case. Sometimes, moving from simulator to device to macOS target (or what have you) can sidestep these Instruments idiosyncrasies. If you can create a reproducible example of the problem, I would suggest posting a bug report.
A word of warning: This will not perform the calculations concurrently, because you await each value returned by iterate. It will not displayResult, much less proceed to the next y until the awaited value is returned. This is effectively performing a serial calculations (and with a lot of overhead, making it even slower than a simple serial calculation).
We can demonstrate the serial behavior by adding some instrumentation:
import os.log
let poi = OSSignposter(subsystem: "Experiment", category: .pointsOfInterest)
nonisolated func iterate(_ x: Int, _ y: Int) -> Int {
let state = poi.beginInterval(#function, id: poi.makeSignpostID(), "\(x), \(y)")
defer { poi.endInterval(#function, state) }
…
return result
}
And when I profile that, I can see that iterate is not running in parallel:
Look at the “Points of Interest” tool. It demonstrates that iterate clearly in not running in parallel. (As an aside, I am not seeing the continuous growth in “Alive Tasks”, so maybe you can post a reproducible example and details about the device/simulator, what iterate is doing, etc.)
But if I use a task group…
func generateWithTaskGroup() async {
await withTaskGroup(of: Void.self) { group in
for x in 0 ..< bitmap.width{
for y in 0 ..< bitmap.height{
group.addTask { [self] in
let result = self.iterate(x,y)
displayResult(result)
}
}
}
}
}
FWIW, I now do see the “Alive Tasks” continuously grow, but I'd expect that when creating so many child tasks. Generally we would limit the number of tasks like outlined in Limiting concurrent tasks in TaskGroups of the Beyond the basics of structured concurrency video. Just like we strove to prevent thread explosion in GCD, you do not want to create thousands or millions of tasks.
These computationally intensive routines should be pulled out of Swift concurrency. Notably, in that video you reference, they recommend that we keep this sort of code in GCD and then bridge back with a continuation:
Admittedly, in that excerpt, they are focusing on blocking API, but as the aforementioned async-await proposal outlined, “long computations” should be kept out of the cooperative thread pool.
Finally, I would also suggest (a) striding (rather than having each call process a single pixel, process an array of pixels); and (b) using concurrentPerform, to avoid overcommitting your device. If you are interested in seeing an example of this, let me know.