在哪里以及为什么会出现僵局?

Where and why deadlock?

我有 2 个并发队列:

let concurrentQueue = DispatchQueue(label: "test.concurrent", attributes: .concurrent)
let syncQueue = DispatchQueue(label: "test.sync", attributes: .concurrent)

和代码:

for index in 1...65 {
    concurrentQueue.async {
        self.syncQueue.async(flags: .barrier) {
            print("WRITE \(index)")
        }
        
        self.syncQueue.sync {
            print("READ \(index)")
        }
    }
}

输出:

WRITE 1
READ 1

为什么、在哪里以及如何陷入僵局?

<65 次迭代计数一切都很好。

< 65 的观察立即让我认为您正在达到队列宽度限制,该限制没有记录(有充分的理由),但被广泛理解为 64。不久前,我写了一个非常详细的答案队列宽度限制 over here。你应该检查一下。

我确实有一些相关的想法可以分享:

我建议的第一件事是用不会触发 I/O 的东西替换 print() 调用。您可以创建一个 numReads 变量和一个 numWrites 变量,然后使用类似原子比较和交换操作的方法来递增它们,然后在循环完成后读取它们并确保它们符合您的预期.参见 Swift 原子学 over here。您也可以将打印操作(异步)分派到主队列。这也将消除任何 I/O 问题。

我还要在这里指出,通过在外部 for 循环的每次迭代中引入一个 barrier 块,您实际上是在使该队列串行化,但是 non-deterministic 命令。至少,您正在为它制造大量争用,这是次优的。 Reader/Writer 当读取次数多于写入次数时,锁往往最有意义,但是您此处的代码具有 1:1 的读取与写入比率。如果这就是您的实际用例,您应该只使用串行队列(或互斥锁),因为它会达到相同的效果,但没有争用。

冒着成为“那个人”的风险,你能否详细说明一下你想在这里完成的工作?如果您只是想向自己证明 reader/writer 使用屏障块模拟的锁有效,我可以向您保证它们确实有效。

这种模式(带屏障的异步写入,并发读取)被称为“reader-writer”模式。这种特殊的多线程同步机制在线程爆炸的情况下可能会死锁。

简而言之,它死锁是因为:

  • 你有“线程爆炸”;

  • 你已经耗尽了只有64个线程的工作线程池;

  • 你的调度项有两个潜在的阻塞调用,不仅是 sync,显然可以阻塞,还有并发的 async(见下一点);和

  • 当您进行分派时,如果池中没有可用的工作线程,它将等待直到有一个可用(即使是异步分派)。

关键的观察是,应该简单地避免肆无忌惮的线程爆炸。通常我们会使用诸如 GCD 的 concurrentPerform (a parallel for loop which is constrained to the maximum number of CPU cores), operation queues (which can be controlled through judicious maxConcurrentOperationCount 设置)或 Swift 并发性(使用其协作线程池来控制并发度、同步角色等)的工具。


虽然 reader-writer 具有直观的吸引力,但在实践中它只是引入了复杂性(多线程环境与另一种多线程机制的同步,两者都受到令人惊讶的小 GCD 工作线程池的限制),没有很多实用的好处。对其进行基准测试,您会发现它比简单的串行 GCD 队列快得可以忽略不计,并且比 lock-based 方法慢得多。