什么时候使用 Semaphore 而不是 Dispatch Group?

When to use Semaphore instead of Dispatch Group?

我假设我知道如何使用 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 一起工作时 - 可能 - 处理问题。

我缺少的部分是什么?

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

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

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

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

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

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

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

另一个典型的例子是producer-consumer problem,其中signalwait调用实际上是两个不同函数的一部分。一种产生数据,一种消耗数据。

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

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

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))

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

当您想设置一次在某些共享资源上运行的线程数的最大值时,信号量很有用。一种常见的用法是当最大值为 1 时,因为共享资源需要独占访问。

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

Conceptually, both of DispatchGroup and Semaphore serve the same purpose (unless I misunderstand something).

以上说法不完全正确。您可以使用信号量来做与调度组相同的事情,但它更通用。

当您有大量想做的事情可以同时发生,但您需要等待它们全部完成后再做其他事情时,使用调度组。

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

一般来说,信号量有一个非负整数的值和两个操作:

  • 等待如果值不为零,则递减它,否则阻塞直到信号量发出信号。

  • 信号如果有线程在等待,解除其中一个线程的阻塞,否则增加值。

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

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

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

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

您几乎可以用信号量做其他更高级别的同步抽象可以做的任何事情,但是正确地做这件事通常是一件棘手的事情。更高级别的抽象是(希望)仔细编写的,如果可能的话,您应该优先使用它们而不是 "roll your own" 带有信号量的实现。

上面 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()
}

一般semaphore可以认为主要是为了解决临界区问题。锁定某些资源以实现同步。另外,如果调用 sleep() 会发生什么,我们可以通过使用信号量来实现同样的事情吗?

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