A lot of people are asking you how it's possible you're getting deadlocks, so I'll reply to all of them here: It's definitely possible without doing anything wrong in your own code, if you're calling poorly written code in other frameworks. See: https://forums.swift.org/t/deadlock-when-using-dispatchqueue...
The important quote:
> both Swift concurrency and Dispatch’s queues are serviced by same underlying pool of threads — Swift concurrency’s jobs just promise not to block on future work
What this means, is that if you're in a Swift concurrency context, and you dispatch_async work to a concurrent queue, then use a semaphore (or similar) to block on that work completing, then the thread pool implementation will not backfill the blocked thread. Crucially, this is true even if the code doing the semaphore hack is old ObjC code that used to work fine.
So if some older code you happen to be calling is doing something like:
func badIdea() {
let sem = DispatchSemaphore()
someConcurrentQueue.async {
doLongRunningThing(completion: { sem.signal() })
}
sem.wait()
}
and you happen to call `badIdea()` from all cores simultaneously, you'll deadlock.
Now, under normal pre-Swift-Concurrency circumstances, GCD would spawn a new thread to handle the queue.async block, which would free the semaphore (this leads to thread explosion, but at least not deadlocks.) But if the call to `badIdea()` happens to be done by a Swift Concurrency Task, then the thread pool gets a hint saying "don't worry, this thread will never block on future work", so it doesn't spawn a new thread to handle the dispatch_async, and you're hosed.
How exposed you are to this issue depends on what kind of code you're calling (third party, even code written by Apple) that may be doing this semaphore hack. You don't have to do this semaphore hack yourself, for this to be a problem. You just have to call into poorly written framework code which may be doing this.
Now, the answer to this problem is that "nobody should write code that does this", which is absolutely true, but it also is the case that there's a lot of code which does it anyway. A lot of people run into this function-coloring issue (which existed before `async/await` was a thing, completion-based functions have the exact same problem) and find themselves painted into a corner where they need to be synchronous, but they need to call asynchronous code, and using a semaphore works, so they just do it and ship it. Swift Concurrency rather silently changes the contract here so that stuff that used to be "merely" a bad idea, is now a deadlock.
This is exactly it, a lot of apple framework code under the hood is not safe and you don't notice it most of the time, but you get users reporting it and maybe reproduce it intermittently once a week if that.
Most apps are not as intense as ours, they are web app equivalents that just do a few http calls and display form data. Our app is pretty intense with gpu background jobs, image queries, ai models running, local db modifications and network uploads occurring, which is way more of a concurrency stress test than most. It acts like a local only desktop app with some optional internet features.
It’s the observability blocking that is the worst part. If we could observe deadlock states then it wouldn't be as bad.
The important quote:
> both Swift concurrency and Dispatch’s queues are serviced by same underlying pool of threads — Swift concurrency’s jobs just promise not to block on future work
What this means, is that if you're in a Swift concurrency context, and you dispatch_async work to a concurrent queue, then use a semaphore (or similar) to block on that work completing, then the thread pool implementation will not backfill the blocked thread. Crucially, this is true even if the code doing the semaphore hack is old ObjC code that used to work fine.
So if some older code you happen to be calling is doing something like:
and you happen to call `badIdea()` from all cores simultaneously, you'll deadlock.Now, under normal pre-Swift-Concurrency circumstances, GCD would spawn a new thread to handle the queue.async block, which would free the semaphore (this leads to thread explosion, but at least not deadlocks.) But if the call to `badIdea()` happens to be done by a Swift Concurrency Task, then the thread pool gets a hint saying "don't worry, this thread will never block on future work", so it doesn't spawn a new thread to handle the dispatch_async, and you're hosed.
How exposed you are to this issue depends on what kind of code you're calling (third party, even code written by Apple) that may be doing this semaphore hack. You don't have to do this semaphore hack yourself, for this to be a problem. You just have to call into poorly written framework code which may be doing this.
Now, the answer to this problem is that "nobody should write code that does this", which is absolutely true, but it also is the case that there's a lot of code which does it anyway. A lot of people run into this function-coloring issue (which existed before `async/await` was a thing, completion-based functions have the exact same problem) and find themselves painted into a corner where they need to be synchronous, but they need to call asynchronous code, and using a semaphore works, so they just do it and ship it. Swift Concurrency rather silently changes the contract here so that stuff that used to be "merely" a bad idea, is now a deadlock.