.sync in concurrent queue

Hi,
Why this piece of code works?

let queue = DispatchQueue(label: "queue_label", attributes: .concurrent)
queue.sync {
    queue.sync {
        print("wokrs!")
    }
}

shouldn't be a deadlock here?
Or maybe "attributes: .concurrent" makes .sync and .async always be an "async"?
Thanks! 🙂

Accepted Reply

Synchronous vs. asynchronous and concurrent vs. serial are two separate concepts. Synchronous vs. asynchronous is about when the caller can continue. Concurrent vs. serial is about when the dispatched task can run.


The outer queue.sync() call will not return until the closure it is submitting has run to completion. The closure might be run right on that same thread or it might run on some worker thread maintained by GCD.


The inner queue.sync() call, again, will not return until the closure it is submitting has run to completion. However, since the queue is concurrent, there's nothing preventing the inner closure from running just because the outer closure is also running. Concurrent queues allow multiple tasks to run at the same time.


Concurrency is not the same as asynchronicity. Each queue.sync() call still doesn't return until the submitted closure has completed.


If the queue were serial, then, yes, your code would cause a deadlock.

Replies

Synchronous vs. asynchronous and concurrent vs. serial are two separate concepts. Synchronous vs. asynchronous is about when the caller can continue. Concurrent vs. serial is about when the dispatched task can run.


The outer queue.sync() call will not return until the closure it is submitting has run to completion. The closure might be run right on that same thread or it might run on some worker thread maintained by GCD.


The inner queue.sync() call, again, will not return until the closure it is submitting has run to completion. However, since the queue is concurrent, there's nothing preventing the inner closure from running just because the outer closure is also running. Concurrent queues allow multiple tasks to run at the same time.


Concurrency is not the same as asynchronicity. Each queue.sync() call still doesn't return until the submitted closure has completed.


If the queue were serial, then, yes, your code would cause a deadlock.

Thanks Ken, I think you opened my eyes 🙂
In documentation to dispatch_sync there is this sentecne:
"As an optimization, this function invokes the block on the current thread when possible."
So, if it is serial queue then it can run on current thread and also on other thread but concurrent only on other thread? And sync and async has nothing in common with that?


Whether this two pieces of code will behave identically, or there is something more?

let queue = DispatchQueue(label: "queue_label") // serial queue
for i in 0..<10 {
    queue.sync { // sync
        print(i)
    }
}


let queue = DispatchQueue(label: "queue_label", attributes: .concurrent) // concurrent queue
for i in 0..<10 {
    queue.sync(flags: .barrier) { // sync with barrier
        print(i)
    }
}

Thank you! 🙂

You're welcome.


Regarding that sentence from the dispatch_sync() documentation: first, whether it runs on the current thread or another thread should not be important to you. Second, it might run it on the current thread whether the queue is serial or concurrent. For a serial queue, the task maybe can't run immediately if the queue is already running some other task. So the current thread will be put to sleep. When the queue is ready to run your task, it could run it on one of its worker threads that is currently scheduled on the CPU and then wake your thread when it's done, or it could wake your thread and have it run the task. For a concurrent queue, it's much more likely to run it on the current thread because that thread is ready and already scheduled on the CPU and there's no reason for it to wait (because the queue is concurrent).


The task will never run on the current thread if you use an async method. That's the whole point of async dispatch: it allows the calling thread to go on to other work while the task executes.


As to your other question: yes, those two pieces of code will behave identically. If you always specify .barrier, you've essentially turned your concurrent queue into a serial queue. I would guess that the concurrent queue code is slightly less efficient than the serial queue code.

"The task will never run on the current thread if you use an async method. That's the whole point of async dispatch: it allows the calling thread to go on to other work while the task executes."
Of course! 🙂 my bad!
Mean while I edited my question but I will put changes here:

1. Why the is a dead lock?


let queue = DispatchQueue(label: "queue_label")
queue.sync {
    queue.sync {
        // Dead lock
    }
}


I read that when there is a serial queue and two nested sync exceutes like in example above, first sync() will execute normally, byt second sync() will stop current thread (because it needs to wait for completion) which is the same as thread of a second sync(), so second sync() can never complete - dead lock
But now this has no sense for me, because serial queue can execute second sync() on other thread, right?

With a serial queue, it will never execute a second task while there's already one executing. That's the definition of "serial". The threads don't matter.


The first task is what calls the inner queue.sync(). So, the first task is still executing at that point and the queue can't start another task until it completes. However, because the inner queue.sync() is synchronous, the first task can't complete until the second task (the one it is submitting) completes. But the second task can't even start, let alone complete, until the first task completes and frees up the queue to run something else.


By the way, if you change the outer queue.sync() to queue.async(), you still cause a deadlock. Your thread can continue, but whatever worker thread runs the outer task will deadlock and that serial queue will never run anything else. The important thing is dispatching a task synchronously to a serial queue from a task which is running on that same serial queue.

"ith a serial queue, it will never execute a second task while there's already one executing. That's the definition of "serial". The threads don't matter."
Yes, I was wondering about that and just figured it out and wanted to write it here, but again, you were faster!
Thank you very much for all informations! It really helped me to understand the differences! 🙂
Best regards!

One thing I can't fully understand, why this code don't cause a deadlock:

let queue = DispatchQueue(label: "queue_label")

print("\(Thread.current)") // <_NSMainThread: 0x60000051c440>{number = 1, name = main}

queue.sync {
    self.view.backgroundColor = .red // ok
		print("in sync \(Thread.current)") // <_NSMainThread: 0x60000051c440>{number = 1, name = main}
}

sync task performed by serial main thread - we see it by print statement and we can additionally prove it by successfully change UI (backgroundColor). If we change queue.sync to DisptachQueue.main.sync - we of course get a deadlock. What's the difference here?

Lemme start with a link to Avoid Dispatch Global Concurrent Queues because it’s relevant to the stuff upthread. However, your question isn’t about concurrent queues, so let’s deal with that…

One thing I can't fully understand, why this code don't cause a deadlock:

The result you’re seeing depends on two key points:

  • Threads and queues are different things. When you enqueue a block on a queue, Dispatch arranges to have a thread run it at some point. In the async case, Dispatch uses one of its worker threads to run that block.

  • But the sync case is different. Here Dispatch knows that your thread is not going to be doing anything useful which waiting for that block to complete, so it uses your thread to run the block.

In your example you call sync(…) from the main thread and so it’s the main thread that runs the block. And your UI manipulation works because it’s done from the main thread.

Having said that, this sort of thing can get you into trouble. The most obvious problem is if some other thread is executing a long-running block that was dispatched to this queue. In that case the main thread will block waiting for that thread to finish before it runs your code. If that delay is long enough, it can result in a poor user experience.

Share and Enjoy

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

Threads and queues are different things. When you enqueue a block on a queue, Dispatch arranges to have a thread run it at some point.

So, difference between queue.sync and DisptachQueue.main.sync in my case is that when using queue.sync, Dispatch sees that some work needs to be executed on a serial queue and wants to be executed synchronously, it knows that now we are on the main thread and that thread is not going to be doing anything useful which waiting for that block to complete, so it schedules this work on main thread after all existed work on this thread is complete. And because of that - there is no deadlock.

On the other hands, when use DisptachQueue.main.sync directly, Dispatch do not wait work to complete on main thread and try to add this block to execute right now - and this cause a deadlock.

How far my conclusions from truth?

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:

  1. The system starts your main thread.

  2. It does the initial work.

  3. Then calls DispatchQueue.main.sync(…).

  4. Dispatch enqueues the work on the main queue.

  5. 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!

I can't still understand relation between main thread and main queue in this scenario. Queue - entity that rules pool of threads entities to execute blocks. Thread - entity that execute actual work. Right?

However, the work never completes because the thing that would be servicing the main queue, the main thread, is blocked waiting for itself.

When we call DispatchQueue.main.sync we said: start doing this peace of work on main queue (and on main thread) and don't return until you finish. And we get deadlock.

When we call myCustomSerialQueue.sync we said: start doing this peace of work on myCustomSerialQueue queue (and on main thread because of sync optimization in our scenario) and don't return until you finish. And here there's no deadlock.

In both cases block runs on main thread.

Difference is that in DispatchQueue.main.sync case under the hood starts some semaphore that block further execution, until incoming block is finished, and it will await indefinitely - we get deadlock and crash.

With myCustomSerialQueue.sync case we just reusing the blocked thread (main thread) to run the block and got no deadlock.

Did I get it right?

Queue - entity that rules pool of threads entities to execute blocks.

Not quite.

Thread - entity that execute actual work.

Right?

Yes.

Regarding that first point, when talking about a serial queue, the queue is also a serialisation primitive. When Dispatch assigns a thread to run a block on that queue, no other thread can start running a block on the same queue. This is ‘obvious’, but has a significant impact when you deal with the main queue [1].

For normal queues, Dispatch doesn’t case which thread it assigns to do the work. It finds a free thread in its thread pool [2] and uses that. This is one reason why thread-local storage is incompatible with Dispatch queues, and you use queue-local storage instead [3].

For the main queue that’s different. Blocks scheduled on the main queue must be run by the main thread. Without that, various subsystems, like our UI frameworks, would fail. If the main thread is busy doing something, Dispatch can’t interrupt it, so it always enqueues the work. When the main thread returns to the run loop [4], the run loop checks for enqueued work and, if any is present, calls Dispatch to run it.

Did I get it right?

Pretty much.

Share and Enjoy

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

[1] It’s also relevant when you start working with target queues, but that’s not in play here.

[2] The thread pool implementation is also complex; but, again, you don’t need to understand the details to understand the Dispatch model.

[3] See here.

[4] We’re assuming a normal app here. In other types of processes, like a a daemon, it’s common for the main thread to call dispatchMain(), and things behave differently in that case.

  • I think I get it now. Thank you for your patience and professionalism!

Add a Comment