Как избежать взрыва потока при использовании DispatchSemaphores?

Чтобы ограничить доступ к общей операции только ограниченному количеству потоков, мы можем использовать DispatchSemaphore из Foundation. Например:

func someFunction(operation: @escaping () -> Void) {
        DispatchQueue.global().async {
            self.semaphore.wait()
            
            operation()
            
            self.semaphore.signal()
        }
    }

let semaphore = DispatchSemaphore(value: 5)

someFunction(1) {
  print("Exexuting add operation")
}

someFunction(2) {
  print("Exexuting add operation")
}
.
.
.
someFunction(n) //64 times

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

Как нам избежать взрыва потоков и отправлять только нужное количество потоков, которые будут выполняться и не блокироваться?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
225
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Чтобы избежать этой проблемы с семафорами, вам следует wait перед отправкой в ​​параллельную очередь. Но вы, вероятно, не хотите wait в текущей теме, так как это заблокирует вызывающего абонента. Итак, я мог бы создать очередь «планировщика» (последовательную) для управления всеми заданиями, добавляемыми в некоторую «процессорную» (параллельную) очередь:

let schedulerQueue = DispatchQueue(label: …)
let processorQueue = DispatchQueue(label: …, attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 5)

func start(block: @escaping () -> Void) {
    schedulerQueue.async {
        semaphore.wait()
        processorQueue.async {
            block()
            semaphore.signal()
        }
    } 
}

Если работа, инициируемая block, сама по себе асинхронна (например, сетевой запрос), то вам просто нужно переместить semaphore.signal() в обработчик завершения этого асинхронного рабочего элемента.


Для полноты картины мы часто вообще избегаем использования семафоров. Есть несколько альтернатив:

  1. Одним из простых вариантов является OperationQueue, который может контролировать степень параллелизма с помощью maxConcurrentOperationCount:

    let processorQueue = OperationQueue()
    processorQueue.name = …
    processorQueue.maxConcurrentOperationCount = 5
    
    func start(block: @escaping () -> Void) {
        processorQueue.addOperation {
            block()
        } 
    }
    

    По общему признанию, если работа, связанная с этой операцией, сама по себе была асинхронной, то вам необходимо реализовать собственный подкласс Operation, который позволит вам управлять зависимостями между асинхронными задачами (например, как это рассматривается в Попытка понять подкласс асинхронной операции). Это может показаться сложным, если вы не сделали этого раньше, но это элегантный устаревший метод управления зависимостями между операциями, которые сами по себе являются асинхронными. Но если работа синхронная, очереди операций позволяют с легкостью ограничить параллелизм.

  2. Еще один классический способ избежать взрыва потока: concurrentPerform:

    DispatchQueue.global().async {
        DispatchQueue.concurrentPerform(iterations: 100) { index in
            …
        }
    }
    

    Это если (а) вы запускаете все задачи в один момент времени (т. е. не добавляете больше позже); и (б) вы просто хотите ограничить степень параллелизма количеством ядер, доступных на вашем процессоре, а не произвольным значением, например. 5. Это очень полезно для шаблонов массово-параллельных вычислений, когда вы просто хотите избежать перегрузки ЦП. См. пример Приложение блоков длинного цикла.

  3. Другой способ решить эту проблему — использовать паттерн maxPublishers Объединения, как описано в Платформа объединения, сериализующая асинхронные операции.

  4. Еще один способ — параллелизм Swift. Например, у вас может быть AsyncChannel для управления очередью задач по мере их поступления и группа задач для ограничения степени параллелизма (как описано во второй части Заставить задачи в параллельном выполнении Swift выполняться последовательно).

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

Выбор того, какой из них лучше, зависит от нескольких факторов: В настоящее время Swift concurrency часто является наиболее подходящим решением. Или, если это кодовая база GCD, очереди операций и concurrentPerform являются распространенными устаревшими решениями.

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

Другие вопросы по теме