Чтобы ограничить доступ к общей операции только ограниченному количеству потоков, мы можем использовать 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 потока, поэтому в системе закончатся потоки, что также называется взрывом потока.
Как нам избежать взрыва потоков и отправлять только нужное количество потоков, которые будут выполняться и не блокироваться?
Проблема взрыва потока связана с тем, что код будет 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()
в обработчик завершения этого асинхронного рабочего элемента.
Для полноты картины мы часто вообще избегаем использования семафоров. Есть несколько альтернатив:
Одним из простых вариантов является OperationQueue
, который может контролировать степень параллелизма с помощью maxConcurrentOperationCount
:
let processorQueue = OperationQueue()
processorQueue.name = …
processorQueue.maxConcurrentOperationCount = 5
func start(block: @escaping () -> Void) {
processorQueue.addOperation {
block()
}
}
По общему признанию, если работа, связанная с этой операцией, сама по себе была асинхронной, то вам необходимо реализовать собственный подкласс Operation
, который позволит вам управлять зависимостями между асинхронными задачами (например, как это рассматривается в Попытка понять подкласс асинхронной операции). Это может показаться сложным, если вы не сделали этого раньше, но это элегантный устаревший метод управления зависимостями между операциями, которые сами по себе являются асинхронными. Но если работа синхронная, очереди операций позволяют с легкостью ограничить параллелизм.
Еще один классический способ избежать взрыва потока: concurrentPerform
:
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 100) { index in
…
}
}
Это если (а) вы запускаете все задачи в один момент времени (т. е. не добавляете больше позже); и (б) вы просто хотите ограничить степень параллелизма количеством ядер, доступных на вашем процессоре, а не произвольным значением, например. 5
. Это очень полезно для шаблонов массово-параллельных вычислений, когда вы просто хотите избежать перегрузки ЦП. См. пример Приложение блоков длинного цикла.
Другой способ решить эту проблему — использовать паттерн maxPublishers
Объединения, как описано в Платформа объединения, сериализующая асинхронные операции.
Еще один способ — параллелизм Swift. Например, у вас может быть AsyncChannel
для управления очередью задач по мере их поступления и группа задач для ограничения степени параллелизма (как описано во второй части Заставить задачи в параллельном выполнении Swift выполняться последовательно).
Все они имеют преимущества перед шаблоном семафора, поскольку предлагают более надежные шаблоны отмены.
Выбор того, какой из них лучше, зависит от нескольких факторов: В настоящее время Swift concurrency часто является наиболее подходящим решением. Или, если это кодовая база GCD, очереди операций и concurrentPerform
являются распространенными устаревшими решениями.
К сожалению, детали их реализации будут немного различаться в зависимости от конкретных требований, поэтому трудно дать более конкретный ответ. Примечательно, что детали различаются в зависимости от того, а) отдельные задания являются синхронными или асинхронными; и (б) будут ли все вакансии добавлены заранее или вы, возможно, добавите больше позже. В итоге, чтобы получить более конкретную информацию, нам может понадобиться несколько дополнительных подробностей о природе того, что происходит в этих параллельных задачах. Но будем надеяться, что вышеизложенное в общих чертах описывает некоторые альтернативы ограниченному параллелизму.