Is there an equivalent of DispatchQueue.concurrentPerform() with the new async/await?

Just wondering if there is an equivalent of DispatchQueue.concurrentPerform() with the new async/await pattern introduced with Swift 5.5?

I'm using concurrentPerform to iterate through all the pixels of an image and was wondering if this is possible with the new async/await pattern too. Would love to do some comparisons on performance.

Accepted Reply

Dispatch's concurrentPerform is a parallelism API and Swift 5.5 introduces concurrency not parallelism. (Please refer to this segment of our previous WWDC talk - https://developer.apple.com/videos/play/wwdc2017/706/?time=196 - for definitions of these concepts and how they differ).

There are no parallelism APIs with Swift concurrency that you can use that have the same behaviour of concurrentPerform. One noteworthy distinction is that concurrentPerform in dispatch is not an asynchronous operation - the caller thread participates in the operation and will block until all the operations in the concurrentPerform are completed.

A TaskGroup might feel like a tempting solution but it provides structured concurrency not parallelism. The dispatch equivalent for a TaskGroup would be to queue.async a bunch of work items to a concurrent queue and group the work items together with a DispatchGroup. It does not semantically provide you with the notion that the DispatchGroup is for a parallel compute workload, which is what concurrentPerform does.

  • I agree re concurrency not being the same thing as parallelism.

    But you appear to suggest that one cannot achieve parallelism with a task group. But we do (and we avoid the thread explosion issues of queue.async while simultaneously enjoying structured concurrency benefits). All of my recent computationally intensive and massively parallel tests demonstrate that task group performance is indistinguishable from concurrentPerform.

    Help me understand why not task group? I see no downside.

Add a Comment

Replies

Dispatch's concurrentPerform is a parallelism API and Swift 5.5 introduces concurrency not parallelism. (Please refer to this segment of our previous WWDC talk - https://developer.apple.com/videos/play/wwdc2017/706/?time=196 - for definitions of these concepts and how they differ).

There are no parallelism APIs with Swift concurrency that you can use that have the same behaviour of concurrentPerform. One noteworthy distinction is that concurrentPerform in dispatch is not an asynchronous operation - the caller thread participates in the operation and will block until all the operations in the concurrentPerform are completed.

A TaskGroup might feel like a tempting solution but it provides structured concurrency not parallelism. The dispatch equivalent for a TaskGroup would be to queue.async a bunch of work items to a concurrent queue and group the work items together with a DispatchGroup. It does not semantically provide you with the notion that the DispatchGroup is for a parallel compute workload, which is what concurrentPerform does.

  • I agree re concurrency not being the same thing as parallelism.

    But you appear to suggest that one cannot achieve parallelism with a task group. But we do (and we avoid the thread explosion issues of queue.async while simultaneously enjoying structured concurrency benefits). All of my recent computationally intensive and massively parallel tests demonstrate that task group performance is indistinguishable from concurrentPerform.

    Help me understand why not task group? I see no downside.

Add a Comment

All of my recent computationally intensive and massively parallel tests demonstrate that task group performance is indistinguishable from concurrentPerform(…).

Cool.

Help me understand why not task group?

I think your benefiting from the fact that the work you’re doing is all CPU bound. In that case you’re correct, TaskGroup and concurrentPerform(…) should perform the same, because both are saturating all cores.

Where things change is when the work is not all CPU bound, where there’s a mixture of CPU and I/O operations. In that case Dispatch will overcommit — that is, it’ll start more worker threads than there are cores — but Swift concurrency won’t, and that’ll leave cores idle waiting for I/O to complete.

Processing work that includes a mixture of CPU and I/O operations optimally is a challenging task. Notably, this challenge is not unique to Swift concurrency. It’s a challenging task in Dispatch as well.

Fortunately, it seems like that’s not the case for you, so yay!

Oh, a couple of hints:

  • In both APIs, make sure that each item of work runs long enough to justify the dispatch overhead. That overhead should be less in Swift concurrency than in Dispatch, but if your work items are just a few instructions then the overhead will dominate in either API.

  • In Swift concurrency specifically, if your tasks run for a long time without any await statements, make sure they at least check for cancellation. You may also want to have them yield.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Agreed on all counts. Thanks for taking the time to clarify!

Add a Comment