信号量的最大值?
Max values of semaphore?
比如有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...
}
一些观察:
您永远不想超过每个 QoS 的最大 GCD 工作线程数。如果超出此范围,您可能会在应用程序中遇到阻塞。我最后检查过,这个限制是 64 个线程。
话虽如此,超过设备上的核心数量通常没有什么好处。
通常,我们会让 GCD 使用 concurrentPerform
为我们算出最大并发线程数,它会自动针对设备进行优化。它还消除了对任何信号量或组的需要,通常会导致代码不那么混乱:
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 1000) { i in
doWork(i)
}
DispatchQueue.main.async {
// go on...
}
}
concurrentPerform
将 运行 并行执行 1,000 次迭代,但将并发线程数限制在适合您设备的级别,从而消除了对信号量的需要。但是 concurrentPerform
本身是同步的,直到所有迭代都完成后才会继续,从而消除了对调度组的需要。因此,将整个 concurrentPerform
分派到某个后台队列,完成后,只需执行您的“完成代码”(或者,在您的情况下,将该代码分派回主队列)。
虽然我在上面争论过 concurrentPerform
,但只有当 doWork
同步执行其任务(例如某些计算操作)时才有效。如果它正在启动本身是异步的东西,那么我们必须回退到这种 semaphore/group 技术。 (或者,也许更好,使用异步 Operation
subclasses with a queue with reasonable maxConcurrentOperationCount
or Combine flatMap(maxPublishers:_:)
并合理限制计数)。
关于本例合理的阈值,没有什么神奇的数字。您只需执行一些经验测试,以在核心数量与您的应用程序中可能发生的其他事情之间找到合理的平衡。例如,对于网络请求,我们通常使用 4 或 6 作为最大计数,不仅考虑到超过该计数的收益减少,而且还考虑到如果成千上万的用户碰巧提交过多的并发请求对我们服务器的影响同时请求。
在“让它变快”方面,“应该允许运行并发多少次迭代”的选择只是decision-making过程的一部分。更关键的问题很快就变成确保 doWork
做足够的工作来证明并发模式引入的适度开销是合理的。
例如,如果处理 1,000×1,000 像素的图像,您可以执行 1,000,000 次迭代,每次处理一个像素。但如果你这样做,你可能会发现它实际上比你的 non-concurrent 再现慢。相反,您可能有 1,000 次迭代,每次迭代处理 1,000 个像素。或者您可能有 100 次迭代,每次处理 10,000 个像素。这种称为“跨步”的技术通常需要进行一些实证研究,以在将执行的迭代次数与每次迭代的工作量之间找到适当的平衡。 (顺便说一下,这种跨步模式通常还可以防止缓存晃动,如果多个线程争用相邻的内存地址,就会出现这种情况。)
与前一点相关,我们经常希望这些不同的线程同步它们对共享资源的访问(保持它thread-safe)。该同步会在这些线程之间引入争用。因此,您需要考虑如何以及何时进行此同步。
例如,与其在 doWork
中进行多次同步,不如让每次迭代都更新一个局部变量(不需要同步),并仅在本地计算时才对共享资源执行同步更新完成。很难抽象地回答这个问题,因为它在很大程度上取决于 doWork
在做什么,但它很容易影响整体性能。
比如有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...
}
一些观察:
您永远不想超过每个 QoS 的最大 GCD 工作线程数。如果超出此范围,您可能会在应用程序中遇到阻塞。我最后检查过,这个限制是 64 个线程。
话虽如此,超过设备上的核心数量通常没有什么好处。
通常,我们会让 GCD 使用
concurrentPerform
为我们算出最大并发线程数,它会自动针对设备进行优化。它还消除了对任何信号量或组的需要,通常会导致代码不那么混乱:DispatchQueue.global().async { DispatchQueue.concurrentPerform(iterations: 1000) { i in doWork(i) } DispatchQueue.main.async { // go on... } }
concurrentPerform
将 运行 并行执行 1,000 次迭代,但将并发线程数限制在适合您设备的级别,从而消除了对信号量的需要。但是concurrentPerform
本身是同步的,直到所有迭代都完成后才会继续,从而消除了对调度组的需要。因此,将整个concurrentPerform
分派到某个后台队列,完成后,只需执行您的“完成代码”(或者,在您的情况下,将该代码分派回主队列)。虽然我在上面争论过
concurrentPerform
,但只有当doWork
同步执行其任务(例如某些计算操作)时才有效。如果它正在启动本身是异步的东西,那么我们必须回退到这种 semaphore/group 技术。 (或者,也许更好,使用异步Operation
subclasses with a queue with reasonablemaxConcurrentOperationCount
or CombineflatMap(maxPublishers:_:)
并合理限制计数)。关于本例合理的阈值,没有什么神奇的数字。您只需执行一些经验测试,以在核心数量与您的应用程序中可能发生的其他事情之间找到合理的平衡。例如,对于网络请求,我们通常使用 4 或 6 作为最大计数,不仅考虑到超过该计数的收益减少,而且还考虑到如果成千上万的用户碰巧提交过多的并发请求对我们服务器的影响同时请求。
在“让它变快”方面,“应该允许运行并发多少次迭代”的选择只是decision-making过程的一部分。更关键的问题很快就变成确保
doWork
做足够的工作来证明并发模式引入的适度开销是合理的。例如,如果处理 1,000×1,000 像素的图像,您可以执行 1,000,000 次迭代,每次处理一个像素。但如果你这样做,你可能会发现它实际上比你的 non-concurrent 再现慢。相反,您可能有 1,000 次迭代,每次迭代处理 1,000 个像素。或者您可能有 100 次迭代,每次处理 10,000 个像素。这种称为“跨步”的技术通常需要进行一些实证研究,以在将执行的迭代次数与每次迭代的工作量之间找到适当的平衡。 (顺便说一下,这种跨步模式通常还可以防止缓存晃动,如果多个线程争用相邻的内存地址,就会出现这种情况。)
与前一点相关,我们经常希望这些不同的线程同步它们对共享资源的访问(保持它thread-safe)。该同步会在这些线程之间引入争用。因此,您需要考虑如何以及何时进行此同步。
例如,与其在
doWork
中进行多次同步,不如让每次迭代都更新一个局部变量(不需要同步),并仅在本地计算时才对共享资源执行同步更新完成。很难抽象地回答这个问题,因为它在很大程度上取决于doWork
在做什么,但它很容易影响整体性能。