【问题标题】:When to use Semaphore instead of Dispatch Group?何时使用 Semaphore 而不是 Dispatch Group?
【发布时间】:2023-03-07 22:38:02
【问题描述】:

我假设我知道如何使用 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 都服务于相同的目的(除非我误解了什么)。

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

我缺少什么?

【问题讨论】:

  • 信号量可用于各种应用程序。但是对于这样的任务,任何调用wait(无论是信号量还是组)通常都是一个坏主意。当你这样做时你正在阻塞一个线程(更糟糕的是,如果你阻塞了主线程,那可能会导致严重的问题)。使用调度组notify 或类似模式几乎总是更好。拥抱异步模式而不是使用wait 来实现同步行为。
  • 我想在信号量的情况下,理论上dq2 有可能在dq1 之前运行,在这种情况下,执行顺序不能保证(与DispatchGroup 不同)。任何人都可以确认/纠正吗?
  • @GuyKogus - dq1dq2 的潜在时机与信号量无关。事实是(a)有两个单独的队列,(b)他正在做async
  • 我刚刚在 Playgrounds 中做了一个快速测试,DispatchQueue.global(qos: .userInitiated) 将返回相同的队列。因此,在这种情况下,如果该队列是串行的,则信号量将不会做任何事情。除非该代码只是一个示例,我们应该想象它们是唯一的队列。
  • @GuyKogus "如果该队列是串行的" ...全局队列不是串行的。

标签: ios swift grand-central-dispatch semaphore


【解决方案1】:

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

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

    • 这是最简洁的解释。我希望这是投票最多的答案!
    • 一个?在我看到 DispatchSemaphore 模拟 DispatchGroup 的所有示例中,初始值为 0。您能解释一下吗?非常感谢!
    • 我绝不是在谈论用信号量模拟一个组。那句话以“One common use…”开头;它不排除其他可能的用途。
    【解决方案3】:

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

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

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

    【讨论】:

    • 这是我的更好的解释:)
    • 两个想法:1. 仅供参考,如果分派的任务本身是同步的,您可以将group.enter()group.leave() 替换为dq1.async(group: group)。 2. 信号量模式很危险,因为如果有数百个任务而不是 3 个任务,您将阻塞所有工作线程。最好将等待调用从async 调用中拉出来,但是有一个任务来执行wait 调用,这样,您最多只能阻塞一个线程并且不能耗尽工作线程。见gist.github.com/robertmryan/deaf98b6095a1545ebdab44d0ef3d5af
    • dq1dq2其实是同一个队列实例,默认qos的全局队列。
    【解决方案4】:

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

    【讨论】:

    【解决方案5】:

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

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

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

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

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

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

    信号量是低级原语,很可能在 GCD 的底层被大量使用。

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

    【讨论】:

    • 队列只要是串行的就可以工作,否则可能会出现竞争条件。
    • @GuyKogus 我应该更准确。大多数人通过将它们添加到串行的主队列来处理这些事情,这与计数1 的信号量相同。例如,对于多生产者问题,这不会有效地工作,因为工作本质上是转移到一个线程。
    【解决方案6】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多