为了将对一个普通操作的访问限制在有限数量的线程中,我们可以使用Foundation的DispatchSemaphore. 例如:

func someFunction(operation: @escaping () -> Void) {
        DispatchQueue.global().async {
            self.semaphore.wait()
            
            operation()
            
            self.semaphore.signal()
        }
    }

let semaphore = DispatchSemaphore(value: 5)

someFunction(1) {
  print("Exexuting add operation")
}

someFunction(2) {
  print("Exexuting add operation")
}
.
.
.
someFunction(n) //64 times

这里的问题是信号量允许5个并发操作,但现在其他线程也被分派,并且分派的线程必须等待前5个线程中的一些线程完成它们的任务.这将导致线程利用率的浪费.此外,DispatchQueue有64个线程的限制,因此系统将耗尽线程,这也称为线程爆炸.

我们如何避免线程爆炸,并只分派正确数量的线程,这些线程将被执行而不是被阻塞?

推荐答案

线程爆炸问题源于这样一个事实,即代码将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 :

  1. 一个简单的选项是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中所考虑的).如果您以前没有这样做,这可能会令人生畏,但这是一种优雅的遗留技术,用于管理本身是异步的操作之间的依赖性.但是,如果工作是同步的,操作队列可以让您轻松地约束并行性.

  2. 另一个避免线程爆炸的classic 方法是concurrentPerform:

    DispatchQueue.global().async {
        DispatchQueue.concurrentPerform(iterations: 100) { index in
            …
        }
    }
    

    这是如果(a)你在一个时间点启动所有任务(即,以后不要再添加);(b)你只想把并发度限制在你的CPU上可用的核心数,而不是任意值,例如5.这对于大规模并行计算模式非常有用,因为在这种模式中,您只想避免过多使用CPU.以Long cycle blocks application为例.

  3. 解决这个问题的另一种方法是使用Combine framework serialize async operations中概述的Combine的maxPublishers模式.

  4. 另一种方法是Swift并发.例如,您可能有一个AsyncChannel来管理任务队列,当它们进入时,还有一个任务组来约束并发程度(如Make tasks in Swift concurrency run serially的后半部分所述).

所有这些都比信号量模式有优势,因为它们提供了更健壮的取消模式.

至于哪一个是最好的取决于几个因素:现在,Swift concurrency通常是"go "的解决方案.或者,如果这是GCD代码库,操作队列和concurrentPerform是常见的遗留解决方案.

不幸的是,这些方法的实现细节会根据具体的要求而有所不同,因此很难得到比这更具体的内容.值得注意的是,如果(a)单个作业(job)是同步的还是异步的;(b)所有作业(job)是预先添加的,还是稍后添加更多作业(job),细节会有所不同.底线是,为了更具体地说,我们可能需要更多的细节来了解这些并行任务的性质.但希望以上内容大致概述了约束并行性的几种替代方案.

Swift相关问答推荐

在解码字符串时需要帮助.

如何将EnvironmentObject从UIKit视图控制器传递到SwiftUI视图

SWIFT中MAP的静态方法包装器

通过SwiftUI中的列表 Select 从字典中检索值

显示第二个操作紧接在另一个操作后的工作表在SwiftUI中不起作用

了解SWIFT中的任务行为

在Xcode SWIFT中运行用Ionic编写的函数

从`compactMap()`闭包返回`nil`未通过结果类型判断

如何避免切换视图递归onChange调用SwiftUI

Result类型的初始值设定项失败?

如何判断设备是否为 Vision Pro - Xcode 15 Beta 4

循环字典中的数组

如何在 UITableView 中点击图片和标题

在主要参与者中启动分离任务和调用非隔离函数之间的区别

在 Swift 中为(递归)扩展提供默认实例参数

Swift:withCheckedContinuation 和 Dispatch QoSClass

我可以在 Swift 中为泛型类型 T 分配默认类型吗?

Swift 中惰性 var 的优势是什么

如何交换快速数组中的元素?

将 [String] 存储在 NSUserDefaults 中