Why can't we use a dispatch_sync on the current queue?
dispatch_sync
does two things:
- queue a block
- blocks the current thread until the block has finished running
Given that the main thread is a serial queue (which means it uses only one thread), if you run the following statement on the main queue:
dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});
the following events will happen:
dispatch_sync
queues the block in the main queue.dispatch_sync
blocks the thread of the main queue until the block finishes executing.dispatch_sync
waits forever because the thread where the block is supposed to run is blocked.
The key to understanding this issue is that dispatch_sync
does not execute blocks, it only queues them. Execution will happen on a future iteration of the run loop.
The following approach:
if (queueA == dispatch_get_current_queue()){
block();
} else {
dispatch_sync(queueA, block);
}
is perfectly fine, but be aware that it won't protect you from complex scenarios involving a hierarchy of queues. In such case, the current queue may be different than a previously blocked queue where you are trying to send your block. Example:
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
// dispatch_get_current_queue() is B, but A is blocked,
// so a dispatch_sync(A,b) will deadlock.
dispatch_sync(queueA, ^{
// some task
});
});
});
For complex cases, read/write key-value data in the dispatch queue:
dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);
static int kKey;
// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ,
&kKey,
(void*)tag,
(dispatch_function_t)CFRelease);
dispatch_sync(workerQ, ^{
// is funnelQ in the hierarchy of workerQ?
CFStringRef tag = dispatch_get_specific(&kKey);
if (tag){
dispatch_sync(funnelQ, ^{
// some task
});
} else {
// some task
}
});
Explanation:
- I create a
workerQ
queue that points to afunnelQ
queue. In real code this is useful if you have several “worker” queues and you want to resume/suspend all at once (which is achieved by resuming/updating their targetfunnelQ
queue). - I may funnel my worker queues at any point in time, so to know if they are funneled or not, I tag
funnelQ
with the word "funnel". - Down the road I
dispatch_sync
something toworkerQ
, and for whatever reason I want todispatch_sync
tofunnelQ
, but avoiding a dispatch_sync to the current queue, so I check for the tag and act accordingly. Because the get walks up the hierarchy, the value won't be found inworkerQ
but it will be found infunnelQ
. This is a way of finding out if any queue in the hierarchy is the one where we stored the value. And therefore, to prevent a dispatch_sync to the current queue.
If you are wondering about the functions that read/write context data, there are three:
dispatch_queue_set_specific
: Write to a queue.dispatch_queue_get_specific
: Read from a queue.dispatch_get_specific
: Convenience function to read from the current queue.
The key is compared by pointer, and never dereferenced. The last parameter in the setter is a destructor to release the key.
If you are wondering about “pointing one queue to another”, it means exactly that. For example, I can point a queue A to the main queue, and it will cause all blocks in the queue A to run in the main queue (usually this is done for UI updates).
I found this in the documentation (last chapter):
Do not call the dispatch_sync function from a task that is executing on the same queue that you pass to your function call. Doing so will deadlock the queue. If you need to dispatch to the current queue, do so asynchronously using the dispatch_async function.
Also, I followed the link that you provided and in the description of dispatch_sync I read this:
Calling this function and targeting the current queue results in deadlock.
So I don't think it's a problem with GCD, I think the only sensible approach is the one you invented after discovering the problem.
I know where your confusion comes from:
As an optimization, this function invokes the block on the current thread when possible.
Careful, it says current thread.
Thread != Queue
A queue doesn't own a thread and a thread is not bound to a queue. There are threads and there are queues. Whenever a queue wants to run a block, it needs a thread but that won't always be the same thread. It just needs any thread for it (this may be a different one each time) and when it's done running blocks (for the moment), the same thread can now be used by a different queue.
The optimization this sentence talks about is about threads, not about queues. E.g. consider you have two serial queues, QueueA
and QueueB
and now you do the following:
dispatch_async(QueueA, ^{
someFunctionA(...);
dispatch_sync(QueueB, ^{
someFunctionB(...);
});
});
When QueueA
runs the block, it will temporarily own a thread, any thread. someFunctionA(...)
will execute on that thread. Now while doing the synchronous dispatch, QueueA
cannot do anything else, it has to wait for the dispatch to finish. QueueB
on the other hand, will also need a thread to run its block and execute someFunctionB(...)
. So either QueueA
temporarily suspends its thread and QueueB
uses some other thread to run the block or QueueA
hands its thread over to QueueB
(after all it won't need it anyway until the synchronous dispatch has finished) and QueueB
directly uses the current thread of QueueA
.
Needless to say that the last option is much faster as no thread switch is required. And this is the optimization the sentence talks about. So a dispatch_sync()
to a different queue may not always cause a thread switch (different queue, maybe same thread).
But a dispatch_sync()
still cannot happen to the same queue (same thread, yes, same queue, no). That's because a queue will execute block after block and when it currently executes a block, it won't execute another one until the currently executed is done. So it executes BlockA
and BlockA
does a dispatch_sync()
of BlockB
on the same queue. The queue won't run BlockB
as long as it still runs BlockA
, but running BlockA
won't continue until BlockB
has ran. See the problem? It's a classical deadlock.