Dispatch Semaphore 会不会无意中陷入死锁?
Can a Dispatch Semaphore inadvertently deadlock itself?
假设我们有一组不同的全局队列可以访问的共享资源,并且为了这个问题,我们使用 Dispatch Semaphore 来管理该访问。当这些全局队列之一告诉信号量等待时,信号量计数递减,并且该线程可以访问共享资源。有没有可能在信号量等待的时候,另一个(不同的)全局队列试图访问这个共享资源,而GCD从它的池中抓取的线程是为前一个队列(当前正在创建的队列)抓取的同一个线程信号量等待)这会死锁这个线程并阻止信号量计数重新递增?
简答:
是的,使用信号量会导致死锁,但不是您建议的原因。
长答案:
如果您有一些已分派的任务在等待信号量,则该工作线程将被阻塞,直到收到信号并恢复执行,随后 returns。因此,您不必担心另一个已分派的任务尝试使用同一个线程,因为该线程已暂时从线程池中删除。您永远不必担心两个已分派的任务试图同时使用同一个线程。那不是死锁风险。
话虽如此,我们必须意识到线程池中工作线程的数量极其有限(目前每个 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
电话联系在一起。 None 调用 signal
的已分派任务将有机会 运行。而且,当你耗尽工作线程时,这通常是一个灾难性的问题,因为任何试图使用 GCD(对于相同的 QoS)的东西都将无法 运行.
顺便说一句,在线程爆炸场景中使用信号量只是导致死锁的一种特殊方式。但为了完整起见,值得注意的是,有很多方法可以用信号量造成死锁。最常见的例子是信号量(或调度组或其他)用于等待一些异步进程,例如
let semaphore = DispatchSemaphore(value: 0)
someAsynchronousMethod {
// do something useful
semaphore.signal()
}
semaphore.wait()
如果 (a) 您 运行 来自主队列,则可能会出现死锁;但是 (b) 异步方法恰好也在主队列上调用它的完成处理程序。这是典型的信号量死锁。
我只使用了上面的线程爆炸示例,因为死锁并不完全明显。但显然有很多方法可以导致信号量死锁。
假设我们有一组不同的全局队列可以访问的共享资源,并且为了这个问题,我们使用 Dispatch Semaphore 来管理该访问。当这些全局队列之一告诉信号量等待时,信号量计数递减,并且该线程可以访问共享资源。有没有可能在信号量等待的时候,另一个(不同的)全局队列试图访问这个共享资源,而GCD从它的池中抓取的线程是为前一个队列(当前正在创建的队列)抓取的同一个线程信号量等待)这会死锁这个线程并阻止信号量计数重新递增?
简答:
是的,使用信号量会导致死锁,但不是您建议的原因。
长答案:
如果您有一些已分派的任务在等待信号量,则该工作线程将被阻塞,直到收到信号并恢复执行,随后 returns。因此,您不必担心另一个已分派的任务尝试使用同一个线程,因为该线程已暂时从线程池中删除。您永远不必担心两个已分派的任务试图同时使用同一个线程。那不是死锁风险。
话虽如此,我们必须意识到线程池中工作线程的数量极其有限(目前每个 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
电话联系在一起。 None 调用 signal
的已分派任务将有机会 运行。而且,当你耗尽工作线程时,这通常是一个灾难性的问题,因为任何试图使用 GCD(对于相同的 QoS)的东西都将无法 运行.
顺便说一句,在线程爆炸场景中使用信号量只是导致死锁的一种特殊方式。但为了完整起见,值得注意的是,有很多方法可以用信号量造成死锁。最常见的例子是信号量(或调度组或其他)用于等待一些异步进程,例如
let semaphore = DispatchSemaphore(value: 0)
someAsynchronousMethod {
// do something useful
semaphore.signal()
}
semaphore.wait()
如果 (a) 您 运行 来自主队列,则可能会出现死锁;但是 (b) 异步方法恰好也在主队列上调用它的完成处理程序。这是典型的信号量死锁。
我只使用了上面的线程爆炸示例,因为死锁并不完全明显。但显然有很多方法可以导致信号量死锁。