在哪里以及为什么会出现僵局?
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 方法慢得多。
我有 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 方法慢得多。