Dipak
Dipak

Reputation: 2433

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")
    }
}

Upvotes: 3

Views: 1032

Answers (2)

skaak
skaak

Reputation: 3018

You are blocking main.

At step 4, which you call from main, main is submitting the block to the queue and waiting for it to finish. But by that time you already submitted the first block (step 1) and it in turn is waiting for main to free up.

EDIT

Note that CSQ is not blocking trying to execute the two blocks you submitted, rather CSQ and main are blocking waiting for one another to finish. I could illustrate this easily if there was an isBusy function on a queue, but since there is not, let's pretend there is and have a look at the code below.

    func serialQueueTest() {
        let customSerialQueue = DispatchQueue(label: "com.test.dipak")

        print("Step 1")
        customSerialQueue.async {
            print("Step 2")

            // Previously
            //    DispatchQueue.main.sync { AAA }
            // same as below pseudo code
            while ( main.isBusy )
            {
                wait ... *without* releasing control
            }
            now, on main, do AAA and then proceed

            print ( "****" )
            print ( "CSQ will now wait for main to finish what it is doing ..." )
            print ( "But note, it does not release control or do something else," )
            print ( "it *blocks* until main is finished. So it deadlocks." )
        }

        print("Step 4")

        // Previously
        //    customSerialQueue.sync BBB
        // replaced with ...
        while ( csq.isBusy )
        {
            wait ... *without* releasing control
        }
        now, on csq, do BBB then proceed

        print ( "****" )
        print ( "Main will now wait for csq to finish what it is doing ..." )
        print ( "But note, it does not release control or do something else," )
        print ( "it *blocks* until csq is finished. So it deadlocks." )
    }

This would also block, even though I only submitted one block to CSQ.

To break the deadlock you could e.g. when you wait release control (in this context, you could call async in stead of sync) or use a different type of lock or wait in a different way for the other one to finish.

EDIT 2

Let me reduce it to its essence.

// This runs on main
            // This runs on csq
csq.async { main.sync // csq now waits on main to free up }
csq.sync              // main now waits on csq to free up

                      // and you have deadlock

Upvotes: 5

Rob Napier
Rob Napier

Reputation: 299275

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.

Upvotes: 4

Related Questions