Situation: I have a data-processing task which can be long-running (from seconds to minutes) and CPU/memory-intensive (no I/O) with lots of parallelisable subprocesses, but is intended to run in the background so that the UI is not impeded. If the user makes a change to their data then the process has to be cancelled and restarted. Only one copy of the process should run at any one time.
Current solution: Most of this is easily achieved using a sequential OperationQueue (QoS = .utility) to cancel any currently running or queued process and schedule a new one. GCD is used for parallelism within the process. This works very well, except that detecting and handling cancellation deep down in the algorithms becomes invasive and messy.
Question: Is this a scenario for which Swift's structured concurrency with Tasks, async, and await is intended to be used? The relative simplicity of cancellation by regular use of Task.checkCancellation() is the biggest incentive, and TaskLocal storage could aid with propagating certain state down into the internal algorithms. However, would the use of Task's thread pool be likely to lead to bad resource starvation with such a long-running process? Are there any guidelines for how frequently Task.yield() ought to be called, if indeed yielding would help in practice.
Initial work suggests that converting everything to use structured concurrency is possible, but it is a big job and I am reluctant to start it for if what I am doing goes against the spirit of Task or any downsides outweigh the benefits w.r.t. GCD.
Sounds like you've already written the code, so you probably want to stay with what works. In general, Swift Concurrency isn't yet suited for such long running tasks since it only operates on a limited width execution pool and tying up some part of that pool for minutes on end is probably a bad idea. What I would probably do is keep your current logic and add a concurrency interface on top (using continuations, probably) so that you can interact and track completion using async await rather than completion handlers.
Thank you for the advice. From what you say, I'll stick with GCD for now. Most of the code is already written, but there is more to do and is at a point where it is convenient to decide whether to persist with GCD or make a switch to structured concurrency.
I have been holding off using Swift's concurrency until it had matured, so I'm still finding my way with it. Converting this code would have been part of the learning process. Fortunately over the past few years I have found the language and tools (XCode) to be remarkably amenable to refactoring and re-writing to use new techniques as they develop (or as I discover them), so if Swift's concurrency is not suited now but may become so in the future then I feel fairly confident that it won't be the end of the world to make the change at that time.
In addition to the nicer async await syntax it unlocks, another reason to expose your API through the concurrency features is that will allow you to swap the innards over to concurrency when something like custom executors becomes available. When that happens it should allow you to gain better compiler insight into the safety of your while keeping your call sites the same.