Search code examples
swiftobjective-cgrand-central-dispatchdispatch-queue

Why two different serial queue creating deadlock in swift?


I have one custom serial queue, in side this calling main queue synchronously. It's creating deadlock. As per my understanding both are independent queues so it should work and both (Step 3 and Step 5) synchronous block should execute. Can anyone explain why deadlock created? Below is my playground code.

func serialQueueTest() {
    let customSerialQueue = DispatchQueue(label: "com.test.dipak")
    
    print("Step 1")
    customSerialQueue.async {
        print("Step 2")
       
        DispatchQueue.main.sync {
            print("Step 3: Inside main sync queue")
        }
    }
    
    print("Step 4")
    customSerialQueue.sync {
        print("Step 5: Inside Custom Serial Queue sync queue")
    }
}

Solution

  • To expand on Tushar Sharma and Dipak's answers, I'll walk through the code step by step in the order it executes.

    // == Scheduling a work item from the main queue ==
    
    // Create a new serial queue
    let customSerialQueue = DispatchQueue(label: "com.test.dipak")
    
    // Create a work item and append it to customSerialQueue.
    // Don't think of this being "in parallel." These are not threads. They're
    // queues. It will run the next time customSerialQueue is scheduled. That might
    // be immediately (if there's an available core), and that might be in the
    // arbitrarily distant future. It doesn't matter what's in this work item. It's
    // just "some work to do."
    customSerialQueue.async { ... }
    
    // On main queue still
    print("Step 4")
    
    // Create a work item, append it to customSerialQueue, and wait for it to
    // complete. As before, it doesn't matter what's in this work item. It's just
    // stuck onto the end of customSerialQueue and will execute when it gets to the
    // front of the queue and the queue is scheduled. Currently it's 2nd in line
    // after the Step 2 work item.
    customSerialQueue.sync { ... }
    

    At this point, main must yield (block). It can't progress and finish the current work item (the one running serialQueueTest) until the Step 5 work item completes.

    Since nothing is now running, the first block on customSerialQueue can run.

    // == Scheduling a work item from customSerialQueue ==
    print("Step 2")
    
    // Create a block, append it the main queue, and wait for it to complete.
    DispatchQueue.main.sync { ... }
    

    As before, customSerialQueue must yield (block). It can't progress and finish the current work item (the one running Step 2) until the Step 3 work item completes. And the Step 3 work item can't be scheduled until main finishes the work item it's currently running (main is a serial queue).

    At this point, main is blocked waiting for the "Step 5" block to complete, and customSerialQueue is blocked waiting for "Step 3" to complete. This is a classic deadlock, and neither task can progress.

    None of the above changes in the presence of multiple cores. GCD queues are about concurrency, not parallelism (and concurrency is not parallelism). They're not about things running "at the same time." They're about scheduling work items. So you should first reason about them as though they were running on a single core. Then you can add-in questions of what happens if two work items run at the same time. But that question doesn't change basic issues of dependencies.