【问题标题】:Max values of semaphore?信号量的最大值?
【发布时间】:2020-11-08 14:29:38
【问题描述】:

例如,有一个 1000 次循环。使其快速、有效且不会导致死锁的最大值是多少?

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.num.loop", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 4)
for i in 1...1000 {
    semaphore.wait()
    group.enter()
    queue.async(group: group, execute: {
        doWork(i)                                    
        group.leave()
        semaphore.signal()
    })            
}

group.notify(queue: DispatchQueue.main) {
    // go on...
}

【问题讨论】:

  • 顺便说一句,(如果我指出明显的问题,我很抱歉)我们还没有讨论发布与调试版本。如果您还没有这样做,请确保您进行了“发布”构建,因为这是经过优化的,并且可能对性能产生比我们讨论过的任何其他内容更显着的影响。

标签: ios swift grand-central-dispatch semaphore dispatch-async


【解决方案1】:

几个观察:

  1. 您永远不想超过每个 QoS 的最大 GCD 工作线程数。如果超过此值,您可能会在应用程序中遇到阻塞。我最后一次检查,这个限制是 64 个线程。

  2. 话虽如此,超过设备上的内核数量通常没有什么好处。

  3. 通常,我们会让 GCD 使用 concurrentPerform 为我们计算出最大并发线程数,它会自动针对设备进行优化。它还消除了对任何信号量或组的需要,通常会导致代码不那么混乱:

    DispatchQueue.global().async {
        DispatchQueue.concurrentPerform(iterations: 1000) { i in
            doWork(i)                                    
        }
    
        DispatchQueue.main.async {
            // go on...
        }
    }
    

    concurrentPerform 将并行运行 1,000 次迭代,但会将并发线程数限制在适合您设备的水平,从而无需信号量。但是concurrentPerform 本身是同步的,直到所有迭代都完成后才会继续,从而消除了对调度组的需要。因此,将整个 concurrentPerform 分派到某个后台队列,完成后,只需执行您的“完成代码”(或者,在您的情况下,将该代码分派回主队列)。

  4. 虽然我在上面论证了 concurrentPerform,但只有在 doWork 同步执行其任务(例如某些计算操作)时才有效。如果它启动的东西本身就是异步的,那么我们必须回退到这种信号量/组技术。 (或者,也许更好的是,使用带有合理maxConcurrentOperationCount 的队列的异步Operation 子类或将flatMap(maxPublishers:_:) 与合理的计数限制相结合。

    关于这种情况下的合理阈值,没有神奇的数字。您只需要执行一些经验测试,以在内核数量和您的应用程序中可能发生的其他事情之间找到合理的平衡。例如,对于网络请求,我们通常使用 4 或 6 作为最大计数,不仅考虑到超过该计数所带来的收益减少,而且还考虑到如果成千上万的用户碰巧提交了太多并发对我们服务器的影响同时请求。

  5. 就“使其快速”而言,“应允许同时运行多少次迭代”的选择只是决策过程的一部分。更关键的问题很快就变成了确保doWork 做足够的工作来证明并发模式引入的适度开销是合理的。

    例如,如果处理 1,000×1,000 像素的图像,您可以执行 1,000,000 次迭代,每次迭代处理一个像素。但是如果你这样做,你可能会发现它实际上比你的非并发再现要慢。相反,您可能有 1,000 次迭代,每次迭代处理 1,000 个像素。或者您可能有 100 次迭代,每次处理 10,000 个像素。这种称为“跨步”的​​技术通常需要进行一些实证研究,以在一个人将执行多少次迭代与每次完成多少工作之间找到适当的平衡。 (顺便说一下,这种跨步模式通常还可以防止缓存晃动,如果多个线程争用相邻的内存地址,就会出现这种情况。)

  6. 与前一点相关,我们经常希望这些不同的线程同步它们对共享资源的访问(以保持线程安全)。这种同步可能会在这些线程之间引入争用。因此,您需要考虑如何以及何时进行此同步。

    例如,与其在doWork 中进行多次同步,不如让每次迭代更新一个局部变量(不需要同步)并仅在完成局部计算后才对共享资源执行同步更新。这个问题很难抽象地回答,因为它很大程度上取决于doWork 正在做什么,但它很容易影响整体性能。

【讨论】:

  • 感谢这么详细的分析,对我帮助很大。合适的比大的好:-)。正如您在第 6 点中提到的,我在更新循环中的局部变量时遇到了问题。我在循环中得到了大约 20 次运行的不正确变量。
  • 你介意看看上面提到的问题stackoverflow.com/q/64745662/4356169
猜你喜欢
  • 1970-01-01
  • 2014-04-07
  • 1970-01-01
  • 2017-06-05
  • 1970-01-01
  • 1970-01-01
  • 2013-03-18
  • 1970-01-01
  • 2015-08-08
相关资源
最近更新 更多