Home > Software engineering >  Why calling `DispatchQueue.main.sync` asynchronously from concurrent queue succeeds but synchronousl
Why calling `DispatchQueue.main.sync` asynchronously from concurrent queue succeeds but synchronousl

Time:10-11

Here I create concurrent queue with .background priority:

let background = DispatchQueue(label: "backgroundQueue",
                               qos: .background,
                               attributes: [],
                               autoreleaseFrequency: .inherit,
                               target: nil)

When I'm trying to call DispatchQueue.main.sync from this queue asynchronously it executes successfully

background.async {
    DispatchQueue.main.sync {
        print("Hello from background async")
    }
}

However, if I'm trying to call DispatchQueue.main.sync from this queue synchronously it causes deadlock

background.sync {
    DispatchQueue.main.sync {
        print("Hello from background sync")
    }
}

Why calling DispatchQueue.main.sync asynchronously from concurrent queue succeeds but synchronously fails?

CodePudding user response:

There are two types of DispatchQueue:

  1. Serial Queue - A work item starts to be executed once the previous one has finished execution
  2. Concurrent Queue - Work items are executed concurrently

It has also two dispatching techniques:

  1. sync - it blocks the calling thread until execution doesn’t finish(your code waits until that item finishes execution)
  2. async - it doesn’t block the calling thread and your code continues executing while the work item runs elsewhere

Note: Attempting to synchronously execute a work item on the main queue results in a deadlock.

For Apple documentation: https://developer.apple.com/documentation/dispatch/dispatchqueue

CodePudding user response:

Quoting apple docs

.sync

This function submits a block to the specified dispatch queue for synchronous execution. Unlike dispatch_async(::), this function does not return until the block has finished

Which means when you first called background.sync { control was on main thread which belongs to a main queue (which is a serialized queue), as soon as the statement background.sync { was executed, controlled stopped at main queue and its now waiting for the block to to finish execution

But inside background.sync { you access the main queue again by referring DispatchQueue.main.sync { and submit another block for synchronous execution which simply prints "Hello from background sync", but the control is already waiting on main queue to return from background.sync { hence you ended up creating a deadlock.

Main Queue is waiting for control to return from background queue which in turn is waiting for Main queue to finish the execution of print statement :|

In fact apple specifically mentions this usecase in its Description

Calling this function and targeting the current queue results in deadlock.

Additional info:

By accessing main queue inside background queue you simply established circular dependency indirectly, if you really wanna test the above statement you can do it simply as

       let background = DispatchQueue(label: "backgroundQueue",
                                       qos: .background,
                                       attributes: [],
                                       autoreleaseFrequency: .inherit,
                                       target: nil)
        background.sync {
            background.sync {
                print("Hello from background sync")
            }
        }

Clearly you are referring background queue inside background.sync which will cause deadlock, which is what apple docs specifies in its description. Your case was slightly different in a sense that you referred to main queue causing the deadlock indirectly

How using async in any one of those statements breaks the dealock?

Now you can use async in either background.async { or in DispatchQueue.main.async and deadlock will break essentially (I am not suggesting which one is correct here, which is correct depends on your need and what are you trying to accomplish, but to break deadlock you can use async in any one of those dispatch statements and you will be fine)

I will just explain why deadlock will break in only one scenario ( You can infer the solution for other case obviously). Let's just say you use

        background.sync {
            DispatchQueue.main.async {
                print("Hello from background sync")
            }
        }

Now main queue is waiting for the block to finish execution which you submitted to background queue for synchronous execution using background.sync and inside background.sync you access main queue again using DispatchQueue.main but this time you submit your block for the asynchronous execution. Hence control will not wait for the block to finish the execution and instead returns immediately. Because there are no other statements in block you submitted to background queue, it marks the completion of task, hence control returns to main queue. Now main queue does processes tasks submitted and whenever it its time to process your print("Hello from background sync") block it prints it.

CodePudding user response:

.sync means it will block currently working thread, and wait until the closure has been executed. So your first .sync will block the main thread (you must be executing the .sync in the main thread otherwise it won't be deadlock). And wait until the closure in background.sync {...} has been finished, then it can continue.

But the second closure blocks the background thread and assign a new job to main thread, which has been blocked already. So these two threads are waiting for each other forever.

But if you switch you start context, like start your code in a background thread, could resolve the deadlock.


// define another background thread
let background2 = DispatchQueue(label: "backgroundQueue2",
                                       qos: .background,
                                       attributes: [],
                                       autoreleaseFrequency: .inherit,
                                       target: nil)
// don't start sample code in main thread.
background2.async {
    background.sync {
        DispatchQueue.main.sync {
            print("Hello from background sync")
        }
    }
}

These deadlock is caused by .sync operation in a serial queue. Simply call DispatchQueue.main.sync {...} will reproduce the problem.

// only use this could also cause the deadlock.
DispatchQueue.main.sync {
    print("Hello from background sync")
}

Or don't block the main thread at the very start could also resolve the deadlock.

background.async {
    DispatchQueue.main.sync {
        print("Hello from background sync")
    }
}

Conclusion

.sync operation in a serial queue could cause permanent waiting because it's a single threaded. It can't be stopped immediately and looking forward for a new job. The job it's doing currently should be done by first, therefore it can start another. That's why .sync could not be used in a serial queue.

  • Related