线程爆炸问题源于这样一个事实,即代码将wait
个内部分派到全局并发队列.这意味着代码在等待信号量之前已经抓住了一个线程,这会很快耗尽有限的工作线程池.
为了避免信号量的这个问题,您应该将调度到并发队列.但是,您可能不想在当前线程上设置wait
,因为这会阻止调用者.因此,我可能会创建一个"调度器"(串行)队列来管理添加到某个"处理器"(并发)队列中的所有作业(job):
let schedulerQueue = DispatchQueue(label: …)
let processorQueue = DispatchQueue(label: …, attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 5)
func start(block: @escaping () -> Void) {
schedulerQueue.async {
semaphore.wait()
processorQueue.async {
block()
semaphore.signal()
}
}
}
如果由block
发起的工作本身是异步的(例如,一个网络请求),那么您只需将semaphore.signal()
移到异步工作项的完成处理程序中.
为了完整性,我们通常会避免使用信号量.有几种 Select :
一个简单的选项是OperationQueue
,它可以用它的maxConcurrentOperationCount
来控制并发度:
let processorQueue = OperationQueue()
processorQueue.name = …
processorQueue.maxConcurrentOperationCount = 5
func start(block: @escaping () -> Void) {
processorQueue.addOperation {
block()
}
}
诚然,如果与此操作相关的工作本身是异步的,那么您必须实现一个自定义的Operation
子类,它允许您管理异步任务之间的依赖性(例如Trying to Understand Asynchronous Operation Subclass中所考虑的).如果您以前没有这样做,这可能会令人生畏,但这是一种优雅的遗留技术,用于管理本身是异步的操作之间的依赖性.但是,如果工作是同步的,操作队列可以让您轻松地约束并行性.
另一个避免线程爆炸的classic 方法是concurrentPerform
:
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 100) { index in
…
}
}
这是如果(a)你在一个时间点启动所有任务(即,以后不要再添加);(b)你只想把并发度限制在你的CPU上可用的核心数,而不是任意值,例如5
.这对于大规模并行计算模式非常有用,因为在这种模式中,您只想避免过多使用CPU.以Long cycle blocks application为例.
解决这个问题的另一种方法是使用Combine framework serialize async operations中概述的Combine的maxPublishers
模式.
另一种方法是Swift并发.例如,您可能有一个AsyncChannel
来管理任务队列,当它们进入时,还有一个任务组来约束并发程度(如Make tasks in Swift concurrency run serially的后半部分所述).
所有这些都比信号量模式有优势,因为它们提供了更健壮的取消模式.
至于哪一个是最好的取决于几个因素:现在,Swift concurrency通常是"go "的解决方案.或者,如果这是GCD代码库,操作队列和concurrentPerform
是常见的遗留解决方案.
不幸的是,这些方法的实现细节会根据具体的要求而有所不同,因此很难得到比这更具体的内容.值得注意的是,如果(a)单个作业(job)是同步的还是异步的;(b)所有作业(job)是预先添加的,还是稍后添加更多作业(job),细节会有所不同.底线是,为了更具体地说,我们可能需要更多的细节来了解这些并行任务的性质.但希望以上内容大致概述了约束并行性的几种替代方案.