so it schedules this work on main thread after all existed work on
this thread is complete
That’s true for all (serial) queues. The key difference is the thread doing the work. Imagine this naïve implementation of sync(…)
:
extension DispatchQueue {
func naiveSync(_ body: @escaping () -> Void) {
let sem = DispatchSemaphore(value: 0)
self.async {
body()
sem.signal()
}
sem.wait()
}
}
This would actually work, but it’s extremely wasteful. The thread that calls naiveSync(…)
ends up blocking waiting for some other thread to run body
and signal the semaphore. You have two threads in play, with one of them just blocked.
As an optimisation, the real sync(…)
method avoids this problem entirely by reusing the blocked thread to run the body. It waits for the queue to be unlocked, locks it, runs the work, and then unlocks it.
Most of the time that doesn’t matter — all the threads involved are Dispatch worker threads and those are more-or-less equivalent — but the main thread has a specific identity and that can cause some confusion.
when use DispatchQueue.main.sync
directly, Dispatch do not wait work
to complete on main thread
That’s not how I’d put it. Rather, this is tied to the relationship between the main thread and the main queue. When you run a block on the main queue, Dispatch has to ensure that it’s run by the main thread. There are two reasons for this:
-
The block has to be serialised with respect to the main thread. You can’t have them both running code at the same time.
-
There’s tonnes of code that checks whether it’s running on the main thread and fails if it’s not. For that code to work, main queue work has to be run by the main thread.
So, Dispatch disables its sync(…)
optimisation for the main queue. Rather, it runs something more like the naïve code I showed above.
For that code to work there must be something on the main thread that grabs blocks from the main queue and executes them. For GUI apps this is the run loop [1]. Every time you cycle around the run loop, it checks for work enqueued on the main queue and runs it.
Note If you’re not familiar with run loops, the best [2] explanation out there is my [3] WWDC 2010 talk. See WWDC 2010 Session 207 Run Loops Section.
So, in your deadlock case here’s what happens:
-
The system starts your main thread.
-
It does the initial work.
-
Then calls DispatchQueue.main.sync(…)
.
-
Dispatch enqueues the work on the main queue.
-
And blocks waiting for it to complete.
However, the work never completes because the thing that would be servicing the main queue, the main thread, is blocked waiting for itself.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] Daemon code often has no run loop and instead uses dispatchMain()
. That has a very different way of handling main queue work than the run loop.
[2] I am, of course, extremely biased.
[3] Told you I was biased!