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

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

这里我创建了 .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 成功但同步失败?

引用苹果文档

.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

这意味着当你第一次调用 background.sync { control 是在属于主队列(这是一个序列化队列)的主线程上时,一旦语句 background.sync { 被执行,control 就会停止在主队列中,它现在正在等待块完成执行

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

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

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

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

附加信息:

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

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

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

如何在这些语句中的任何一个中使用 async 来中断解除锁定?

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

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

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

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

有两种类型的 DispatchQueue:

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

它还有两种调度技术:

  1. sync - 它会阻塞调用线程直到执行未完成(您的代码会等待该项目完成执行)
  2. async - 它不会阻塞调用线程,并且您的代码会在工作项在别处运行时继续执行

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

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

.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 不能在串行队列中使用的原因。

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

然而,这不是问题,这才是真正的问题:

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

Important

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

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