Search code examples
iosswiftxcodegrand-central-dispatch

What happens when you dispatch a task asynchronously inside a sync queue in Swift?


I have found this to produce a deadlock, but I can't figure out why. Basically, I have a queue inside a class and every time the state of the class is supposed to be changed, I run that task inside the queue as a synchronous task:

private var serialQueue = DispatchQueue(label: "my_mutex_queue")
func changeState() {
  serialQueue.sync {
    // perform change
  }
}

There are certain changes of state that require a call to a delegate. In this case, the task cannot be called synchronously, because it will cause a deadlock. However, dispatching it asynchronously also results in a deadlock (we are still inside the synchronous task "changeState" in the queue "my_mutex_queue"):

func notifyDelegate() {
        serialQueue.async { 
            // notify delegate
        }
}

If I run the delegate notification as an asynchronous task in a different queue, then everything works as expected. I couldn't find any note on Apple's documentation on why calling an asynchronous task inside the same queue causes a deadlock.


Solution

  • You can’t call serialQueue.sync from the block that is being executed by the serialQueue.

    TL;DR;

    Here is what I think is likely happening:

    1. You schedule a block A via serialQueue.async from notifyDelegate.
    2. In the context of block A execution, your delegate calls changeState, incorrectly assuming that current thread is not the serialQueue’s thread.
    3. From the changeState method, being on the serialQueue’s call stack, you schedule synchronously another block B via serialQueue.sync which can never start because you wait for it to be started in the previously asynchronously scheduled block A which is currently executed by the serialQueue.

    Ways to avoid this situation:

    1. Never invoke public callbacks in the private serial queue that you use for synchronization.

    OR

    1. Don’t use private queue for synchronization, use os_unfair_lockor NSLock or NSRecursiveLock instead. It might also improve the performance.