Когда использовать семафор вместо 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, так и семафор служат одной и той же цели (если я не ошибаюсь нечто.)

честно говоря, я не знаком с: когда использовать семафор, особенно когда работа с DispachGroup-вероятно-обрабатывает проблему.

какая часть, которую я пропускаю?

4 ответов


концептуально, как DispachGroup, так и Semaphore служат одной и той же цели (если я что-то не понимаю).

вышесказанное не совсем верно. Вы можете использовать семафор для того же, что и группа отправки, но он гораздо более общий.

группы отправки используются, когда у вас есть нагрузка вещей, которые вы хотите сделать, которые могут произойти одновременно, но вам нужно дождаться их завершения, прежде чем делать что-то еще.

семафоры can использоваться для вышеизложенного, но они являются объектами синхронизации общего назначения и могут использоваться для многих других целей. Концепция семафора не ограничивается Apple и может быть найдена во многих операционных системах.

В общем случае семафор имеет значение, которое является неотрицательным целым числом и двумя операциями:

  • ждать если значение не равно нулю, уменьшите его, иначе заблокируйте, пока что-то не подаст сигнал семафор.

  • сигнал если есть потоки, ожидающие, разблокируйте один из них, в противном случае увеличьте значение.

Излишне говорить, что обе операции должны быть потокобезопасными. В старые времена, когда у вас был только один процессор, вы просто отключали прерывания, манипулируя значением и очередью ожидающих потоков. В настоящее время это сложнее из-за нескольких ядер процессора и на чип-кэшах и т. д.

семафор может использоваться в любом случае, когда у вас есть ресурс, к которому одновременно может получить доступ не более N потоков. Вы устанавливаете начальное значение семафора в N, а затем первые N потоков, которые ждут его, не блокируются, но следующий поток должен ждать, пока один из первых n потоков не просигнализирует семафор. Самый простой случай-N = 1. В этом случае семафор ведет себя как блокировка мьютекса.

семафор может использоваться для эмуляции группы отправки. You start the sempahore at 0, начать все задачи-отслеживание, сколько вы запустили и ждать на семафоре, что количество раз. Каждая задача должна сигнализировать о завершении семафора.

тем не менее, есть некоторые gotchas. Например, вам нужно отдельное количество, чтобы знать, сколько раз ждать. Если вы хотите добавить больше задач в группу после начала ожидания, счетчик может быть обновлен только в блоке, защищенном мьютексом, и это может привести к проблемам с блокировкой. Кроме того, я думаю, что отправка реализация семафоров может быть уязвима для инверсия приоритетов. Инверсия приоритета происходит, когда поток с высоким приоритетом ожидает ресурс, захваченный с низким приоритетом. Поток с высоким приоритетом блокируется до тех пор, пока поток с низким приоритетом не освободит ресурс. Если выполняется поток среднего приоритета, это может никогда не произойти.

вы можете в значительной степени сделать что-нибудь с семафором, что могут сделать другие абстракции синхронизации более высокого уровня, но делать это право - это часто сложный бизнес, чтобы получить право. Абстракции более высокого уровня (надеюсь) тщательно написаны, и вы должны использовать их вместо "свернуть свою собственную" реализацию с семафорами, если это возможно.


семафоры и группы имеют в некотором смысле противоположную семантику. Оба ведут счет. С семафором, a wait разрешается, когда счетчик не равен нулю. С группой a wait разрешается, когда счетчик равен нулю.

семафор полезен, если вы хотите установить максимальное количество потоков, работающих на каком-либо общем ресурсе одновременно. Обычно используется, когда максимальное значение равно 1, поскольку общий ресурс требует эксклюзивного доступа.

A группа полезна, когда вам нужно знать, когда куча задач все были завершены.


используйте семафор, чтобы ограничить количество одновременной работы в данный момент времени. Используйте группу для ожидания завершения выполнения любого количества параллельных работ.

Если вы хотите отправить три задания в очередь, это должно быть

func performUsingGroup() {
    let dq1 = DispatchQueue.global(qos: .default)
    let dq2 = DispatchQueue.global(qos: .default)
    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")
    }
}

и

func performUsingSemaphore() {
    let dq1 = DispatchQueue.global(qos: .default)
    let dq2 = DispatchQueue.global(qos: .default)
    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()
        }
    }
}

один типичный случай использования семафора-это функция, которая может вызываться одновременно из разных потоков и использует ресурс, который не должен вызываться из нескольких потоков одновременно:

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

в этом случае вы сможете позвонить myFunction из разных потоков, но они не смогут достичь одновременно заблокированный ресурс. Одному из них придется подождать, пока второй закончит свою работу.

семафор держит отсчет, поэтому вы можете фактически позвольте заданному количеству потоков одновременно вводить вашу функцию.

типичным общим ресурсом является вывод в файл.

семафор-не единственный способ решения таких проблем. Можно также добавить код в последовательную очередь, например.

семафоры-примитивы низкого уровня, и, скорее всего, они часто используются под капотом в GCD.

другим типичным примером является производитель-потребитель проблема, где signal и wait вызовы на самом деле являются частью двух разных функций. Тот, который производит данные, и тот, который их потребляет.