何时使用信号量而不是调度组



我假设我知道如何使用DispatchGroup,为了理解这个问题,我已经尝试过:

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
performUsingGroup()
}
func performUsingGroup() {
let dq1 = DispatchQueue.global(qos: .userInitiated)
let dq2 = DispatchQueue.global(qos: .userInitiated)
let group = DispatchGroup()
group.enter()
dq1.async {
for i in 1...3 {
print("(#function) DispatchQueue 1: (i)")
}
group.leave()
}
group.wait()
dq2.async {
for i in 1...3 {
print("(#function) DispatchQueue 2: (i)")
}
}
group.notify(queue: DispatchQueue.main) {
print("done by group")
}
}
}

结果 - 正如预期的那样 - 是:

performUsingGroup() DispatchQueue 1: 1
performUsingGroup() DispatchQueue 1: 2
performUsingGroup() DispatchQueue 1: 3
performUsingGroup() DispatchQueue 2: 1
performUsingGroup() DispatchQueue 2: 2
performUsingGroup() DispatchQueue 2: 3
done by group

对于使用信号量,我实现了:

func performUsingSemaphore() {
let dq1 = DispatchQueue.global(qos: .userInitiated)
let dq2 = DispatchQueue.global(qos: .userInitiated)
let semaphore = DispatchSemaphore(value: 1)
dq1.async {
semaphore.wait()
for i in 1...3 {
print("(#function) DispatchQueue 1: (i)")
}
semaphore.signal()
}
dq2.async {
semaphore.wait()
for i in 1...3 {
print("(#function) DispatchQueue 2: (i)")
}
semaphore.signal()
}
}

并在viewDidLoad方法中调用它。结果是:

performUsingSemaphore() DispatchQueue 1: 1
performUsingSemaphore() DispatchQueue 1: 2
performUsingSemaphore() DispatchQueue 1: 3
performUsingSemaphore() DispatchQueue 2: 1
performUsingSemaphore() DispatchQueue 2: 2
performUsingSemaphore() DispatchQueue 2: 3

从概念上讲,DispachGroup 和 Semaphore 都有相同的目的(除非我误解了什么)。

老实说,我不熟悉:何时使用信号量,尤其是在与DispachGroup合作时 - 可能 - 处理问题。

我缺少哪一部分?

从概念上讲,DispatchGroup 和信号量都有相同的目的(除非我误解了什么)。

以上并不完全正确。您可以使用信号量执行与调度组相同的操作,但它要通用得多。

当您有大量要执行的操作可以同时发生,但您需要等待它们全部完成才能执行其他操作时使用调度组。

信号量可用于上述目的,但它们是通用同步对象,也可用于许多其他目的。信号量的概念不仅限于Apple,可以在许多操作系统中找到。

通常,信号量具有一个非负整数的值和两个操作:

  • 等待如果该值不为零,则递减它,否则阻止,直到有信号量。

  • signal如果有线程在等待,请取消阻止其中一个线程,否则递增该值。

不用说,这两个操作都必须是线程安全的。在过去,当您只有一个 CPU 时,您只需禁用中断,同时操作值和等待线程的队列。如今,由于多个CPU内核和片上缓存等原因,它变得更加复杂。

信号量可用于您拥有最多 N 个线程可以同时访问的资源的任何情况。您将信号灯的初始值设置为 N,然后等待它的前 N 个线程不会被阻塞,但下一个线程必须等待,直到前 N 个线程中的一个发出信号量信号。最简单的情况是 N = 1。在这种情况下,信号量的行为类似于互斥锁。

信号量可用于模拟调度组。您从 0 开始 sempahore,启动所有任务 - 跟踪您启动了多少次并在信号灯上等待该次数。每个任务在完成时都必须发出信号灯信号。

但是,有一些陷阱。例如,您需要一个单独的计数才能知道要等待多少次。如果您希望能够在开始等待后向组添加更多任务,则只能在受互斥锁保护的块中更新计数,这可能会导致死锁问题。另外,我认为信号量的调度实现可能容易受到优先级倒置的影响。当高优先级线程等待低优先级已获取的资源时,会发生优先级倒置。高优先级线程将被阻止,直到低优先级线程释放资源。如果有中等优先级的线程正在运行,则可能永远不会发生这种情况。

你几乎可以用信号量做其他更高级别的同步抽象可以做的任何事情,但做对通常是一项棘手的工作。更高级别的抽象(希望)是精心编写的,如果可能的话,您应该优先使用它们,而不是使用信号量"自己滚动"实现。

信号量和组在某种意义上具有相反的语义。两者都保持计数。使用信号量,当计数不为零时,允许wait继续。对于组,当计数为零时,允许wait继续。

如果要设置一次对某些共享资源运行的最大线程数,则信号量非常有用。一种常见的用法是当最大值为 1 时,因为共享资源需要独占访问权限。

当您需要知道一堆任务何时全部完成时,组很有用。

使用信号量限制给定时间的并发工作量。使用组等待任意数量的并发工作完成执行。

如果您想为每个队列提交三个作业,它应该是

import Foundation
func performUsingGroup() {
let dq1 = DispatchQueue(label: "q1", attributes: .concurrent)
let dq2 = DispatchQueue(label: "q2", attributes: .concurrent)
let group = DispatchGroup()

for i in 1...3 {
group.enter()
dq1.async {
print("(#function) DispatchQueue 1: (i)")
group.leave()
}
}
for i in 1...3 {
group.enter()
dq2.async {
print("(#function) DispatchQueue 2: (i)")
group.leave()
}
}

group.notify(queue: DispatchQueue.main) {
print("done by group")
}
}
performUsingGroup()
RunLoop.current.run(mode: RunLoop.Mode.default,  before: Date(timeIntervalSinceNow: 1))

import Foundation
func performUsingSemaphore() {
let dq1 = DispatchQueue(label: "q1", attributes: .concurrent)
let dq2 = DispatchQueue(label: "q2", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 1)

for i in 1...3 {
dq1.async {
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
print("(#function) DispatchQueue 1: (i)")
semaphore.signal()
}
}
for i in 1...3 {
dq2.async {
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
print("(#function) DispatchQueue 2: (i)")
semaphore.signal()
}
}
}
performUsingSemaphore()
RunLoop.current.run(mode: RunLoop.Mode.default,  before: Date(timeIntervalSinceNow: 1))

Jano 和 Ken 的上述回复是正确的:1) 使用信号量来限制一次发生的工作量 2) 使用调度组,以便在组中的所有任务完成时通知该组。例如,您可能希望并行下载大量图像,但由于您知道它们是繁重的图像,因此您希望一次只能下载两次,因此使用信号量。您还希望在完成所有下载(例如有 50 个下载)时收到通知,因此您使用 DispatchGroup。因此,这不是在两者之间做出选择的问题。您可以在同一实现中使用一个或两个,具体取决于您的目标。这种类型的示例在 Ray Wenderlich 站点上的并发教程中提供:

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .utility)
let semaphore = DispatchSemaphore(value: 2)
let base = "https://yourbaseurl.com/image-id-"
let ids = [0001, 0002, 0003, 0004, 0005, 0006, 0007, 0008, 0009, 0010, 0011, 0012]
var images: [UIImage] = []
for id in ids {
guard let url = URL(string: "(base)(id)-jpeg.jpg") else { continue }

semaphore.wait()
group.enter()

let task = URLSession.shared.dataTask(with: url) { data, _, error in
defer {
group.leave()
semaphore.signal()
}

if error == nil,
let data = data,
let image = UIImage(data: data) {
images.append(image)
}
}

task.resume()
}

一个典型的信号量用例是一个函数,它可以从不同的线程同时调用,并使用不应同时从多个线程调用的资源:

func myFunction() {
semaphore.wait()
// access the shared resource
semaphore.signal()
}

在这种情况下,您将能够从不同的线程调用myFunction,但它们将无法同时访问锁定的资源。其中一个将不得不等到第二个完成工作。

信号量保留计数,因此您实际上可以允许给定数量的线程同时进入您的函数。

典型的共享资源是文件的输出。

信号量不是解决此类问题的唯一方法。例如,您还可以将代码添加到串行队列中。

信号量是低级基元,很可能在 GCD 中大量使用。

另一个典型的例子是生产者-消费者问题,其中signalwait调用实际上是两个不同函数的一部分。一个产生数据,一个消费数据。

一般来说,信号量主要可以认为我们可以解决关键部分问题。锁定特定资源以实现同步。另外,如果调用sleep()会发生什么,我们可以通过使用信号量来实现同样的事情吗?

当我们有多个组操作要执行并且我们需要跟踪或设置彼此依赖关系或在组操作系统任务完成其执行时发出通知时,我们将使用调度组。

最新更新