I tried the semaphore route & it worked…until it didn’t. It seems like Swift Concurrency creates only one thread per system processor core and once you have every core blocked waiting for your async work, your app is deadlocked.
The cooperative thread pool has many threads as there are cores. Ideally, they kernel-mode context switching of the threads is minimal, and all the work is done by user-mode context switching of Tasks, which is much much much faster.
For that to work though, it's imperative that your async code never blocks. If you need to call legacy code that blocks, then you should spin that off in a separate thread using all the existing techniques (e.g. a dispatch queue), and using a continuation to signal the completion of its work.
I don't follow. This is to bridge some legacy code, not a modus operandi for new async code.
@christopherweems said it will deadlock, which will only be the case if BlockingTask is called from a thread within that cooperative thread pool; which is not the intended usage.
Also, isn't that Task initializer creating a new task which will always use the cooperative thread pool? It's been a while and IDR when that thread pool is or isn't used, I need to re-watch that video xD
If you know that you’ll never call your bridging code from the cooperative pool & mark it with @available(*, noasync) I think the approach would work.
This is the feature I’m referencing, it’s only a marker to prevent you from calling it from an async context but it’s new to 5.7. You can leave it off assuming you’re already doing that checking yourself.
@vlastobrecka I tried the same semaphore approach and it did work but now I've seen that on the WWDC session (suggested by @AlexanderM) Swift concurrency: Behind the scenes [check 0h26m] they say that semaphores are unsafe to use with Swift Concurrency.
Semaphores or semaphore-like behavior will cause some really heinous problems; as mentioned before they can lead to deadlocks due to resource exhaustion but also they lead to priority inversions. Can they work in very limited cases? Sure... but I wouldn't suggest they be used in production code interfacing with async/await.
Instead it would be better to emit work items to a singular task running those work items. For example you can yield into a continuation of an AsyncStream the async closure of work to be done. But that works only in cases where the result of that work is Void. The other approach is to use callbacks; run the async work such that it calls back into the synchronous world with the value needed.
Neither of those strategies is perfect but they are by far more performant and better long term solutions than semaphores. I would highly suggest for any real world usage to attempt either using AsyncStreams to emit values into the async code, or use callbacks to indicate values to be used by the synchronous code. Structuring that way will keep you on a much easier path that will be better for the end users of the software.