调度信号量是否会无意中导致自身死锁?



>假设我们有一个共享资源,一堆不同的全局队列可以访问该资源,为了解决这个问题,我们使用调度信号量来管理该访问。当其中一个全局队列告诉信号量等待时,信号量计数将递减,并且该线程有权访问共享资源。是否有可能在信号量等待时,另一个(不同的(全局队列尝试访问此共享资源,并且 GCD 从其池中抓取的线程与为上一个队列(当前正在使信号量等待的队列(抓取的线程相同,这会死锁此线程并阻止信号量计数重新递增?

简短回答:

是的,使用信号量可能会导致死锁,但不是由于您建议的原因。

长答案:

如果某个已调度任务正在等待信号量,则该工作线程将被阻塞,直到收到信号并恢复执行并随后返回。因此,您不必担心另一个分派任务尝试使用同一线程,因为该线程会暂时从线程池中删除。您永远不必担心两个分派任务试图同时使用同一个线程。这不是僵局风险。

话虽如此,我们必须对线程池中的工作线程数量极其有限(目前每个 QoS 64 个(这一事实保持敏感。如果您用尽了可用的工作线程,则调度到 GCD 的任何其他内容(具有相同的 QoS(都无法运行,直到一些以前被阻止的工作线程再次可用。

考虑:

print("start")
let semaphore = DispatchSemaphore(value: 0)
let queue = DispatchQueue.global()
let group = DispatchGroup()
let count = 10
for _ in 0 ..< count {
queue.async(group: group) {
semaphore.wait()
}
}
for _ in 0 ..< count {
queue.async(group: group) {
semaphore.signal()
}
}
group.notify(queue: .main) {
print("done")
}

这工作正常。你有十个工作线程与这wait调用捆绑在一起,然后额外的十个调度块调用signal,你没事。

但是,如果将count增加到 100(这种情况称为"线程爆炸"(,则上述代码将永远不会自行解析,因为signal调用正在等待与所有这些wait调用绑定的工作线程。那些带有signal调用的已分派任务都不会有机会运行。而且,当您耗尽工作线程时,这通常是一个灾难性的问题,因为任何尝试使用 GCD(对于相同的 QoS(都将无法运行。


顺便说一下,在线程爆炸方案中使用信号量只是导致死锁的一种特殊方式。但为了完整起见,值得注意的是,有很多方法可以与信号量死锁。最常见的例子是使用信号量(或调度组或其他(来等待一些异步进程,例如

let semaphore = DispatchSemaphore(value: 0)
someAsynchronousMethod {
// do something useful
semaphore.signal()
}
semaphore.wait()

如果 (a( 从主队列运行它,则可能会死锁;但是 (b( 异步方法碰巧也在主队列上调用其完成处理程序。这是典型的信号量死锁。

我只使用了上面的线程爆炸示例,因为死锁并不完全明显。但显然有很多方法会导致信号量死锁。

最新更新