为啥从并发队列异步调用`DispatchQueue.main.sync`成功但同步失败?

Posted

技术标签:

【中文标题】为啥从并发队列异步调用`DispatchQueue.main.sync`成功但同步失败?【英文标题】:Why calling `DispatchQueue.main.sync` asynchronously from concurrent queue succeeds but synchronously fails?为什么从并发队列异步调用`DispatchQueue.main.sync`成功但同步失败? 【发布时间】:2021-11-29 22:54:05 【问题描述】:

这里我创建了具有 .background 优先级的并发队列:

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

当我尝试从该队列异步调用DispatchQueue.main.sync 时,它会成功执行

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

但是,如果我尝试从该队列同步调用DispatchQueue.main.sync,则会导致死锁

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

为什么从并发队列异步调用DispatchQueue.main.sync会成功而同步失败?

【问题讨论】:

【参考方案1】:

.sync 表示它将阻塞当前工作的线程,并等待直到执行完闭包。所以你的第一个.sync 将阻塞主线程(你必须在主线程中执行 .sync 否则它不会死锁)。并等到background.sync ...中的闭包完成后,才可以继续。

但是第二个闭包阻塞了后台线程并分配了一个新的工作给已经被阻塞的主线程。所以这两个线程永远在等待对方。

但是如果你切换你的启动上下文,比如在后台线程中启动你的代码,可以解决死锁。


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

这些死锁是由串行队列中的.sync 操作引起的。只需调用DispatchQueue.main.sync ... 即可重现该问题。

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

或者一开始不阻塞主线程也可以解决死锁。

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

结论

.sync 串行队列中的操作可能会导致永久等待,因为它是单线程的。它不能立即停止并期待新的工作。它目前正在做的工作应该先完成,然后它可以开始另一个。这就是为什么.sync 不能用于串行队列的原因。

【讨论】:

【参考方案2】:

首先,这是一个串行队列,它不是并发队列,如果你想要一个并发队列,你需要在属性中指定它。

但是,这不是问题,这是实际问题:

截取自DispatchQueue documentation 的屏幕截图,其中包括:

重要

尝试在主队列上同步执行工作项会导致死锁。

结论:永远不会在主队列上调度同步。你迟早会陷入僵局。

【讨论】:

【参考方案3】:

引用苹果文档

.sync

这个函数提交一个block到指定的dispatch queue 同步执行。与 dispatch_async(::) 不同,这个函数确实 在块完成之前不返回

这意味着当您第一次调用 background.sync 时,控制位于属于主队列(这是一个序列化队列)的主线程上,一旦执行语句 background.sync ,控制停止在主队列,现在等待块完成执行

但在background.sync 中,您通过引用DispatchQueue.main.sync 再次访问主队列并提交另一个同步执行块,它只打印“Hello from background sync”,但控件已经在等待主队列从@987654325 返回@ 因此,您最终造成了死锁。

主队列正在等待控制从后台队列返回,而后台队列又在等待主队列完成打印语句的执行:|

事实上,苹果在其描述中特别提到了这个用例

调用此函数并定位当前队列会导致 死锁。

其他信息:

通过访问后台队列中的主队列,你只是间接地建立了循环依赖,如果你真的想测试上面的语句,你可以这样做

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

很明显,您指的是background.sync 内的background 队列,这将导致死锁,这是苹果文档在其描述中指定的。从某种意义上说,您的情况略有不同,因为您提到了间接导致死锁的主队列

在其中任何一个语句中使用 async 如何打破解除锁定?

现在您可以在background.async DispatchQueue.main.async 中使用async 并且死锁将基本上被打破(我不建议这里哪个是正确的,哪个是正确的取决于您的需要以及您要完成什么,但要打破僵局,您可以在任何一个调度语句中使用async,您会没事的)

我将解释为什么死锁只会在一种情况下中断(您显然可以推断出其他情况的解决方案)。假设你使用

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

现在主队列正在等待块完成执行,您使用background.sync 提交到后台队列以进行同步执行,而在background.sync 内部,您使用DispatchQueue.main 再次访问主队列,但这次您提交块以用于异步执行。因此,控制不会等待块完成执行,而是立即返回。由于您提交到后台队列的块中没有其他语句,它标志着任务完成,因此控制返回到主队列。现在主队列确实处理提交的任务,并且每当需要处理您的 print("Hello from background sync") 块时,它就会打印它。

【讨论】:

【参考方案4】:

DispatchQueue 有两种类型:

    串行队列 - 一个工作项在前一个完成执行后开始执行 并发队列 - 工作项同时执行

它还有两种调度技术:

    同步 - 它阻塞调用线程直到执行未完成(您的代码等待该项目完成执行) 异步 - 它不会阻塞调用线程,您的代码会在工作项在其他地方运行时继续执行

注意:尝试在主队列上同步执行工作项会导致死锁。

对于 Apple 文档:https://developer.apple.com/documentation/dispatch/dispatchqueue

【讨论】:

以上是关于为啥从并发队列异步调用`DispatchQueue.main.sync`成功但同步失败?的主要内容,如果未能解决你的问题,请参考以下文章

调度队列异步调用

为啥必须在主队列上异步调用 resignFirstResponder() 来关闭键盘

dispatchqueue 在 Swift 4 中异步使用以加速

带队列的异步/同步并发调用

当更新我的 UI 以响应异步操作时,我应该在哪里调用 DispatchQueue?

使用 DispatchGroup、DispatchQueue 和 DispatchSemaphore 按顺序执行带有 for 循环的 Swift 4 异步调用