Я реализовал настраиваемую параллельную очередь поверх GCD, которая предлагает две дополнительные функции:
- Ограничьте максимальное количество одновременно выполняемых задач.
- Предоставьте вручную контроль над тем, какая задача из очереди запланирована к выполнению следующей.
An OperationQueue
может, конечно, обратиться к первому, но не дает контроля над вторым. Однако возможность влиять на порядок, в котором очередь выполняет свою задачу, полезна для целей моего приложения (ожидание и задержка дорогостоящих задач, которые, вероятно, будут отменены), поэтому я попытался реализовать эту реализацию.
У меня несколько проблем:
Тот факт, что это один предоставление класса два отдельные части функциональности кажутся красным флагом. Вероятно, было бы лучше реализовать это в двух отдельных типах, возможно, с одним подклассом другого. Но я не вижу прямого способа распутать это без чрезмерного усложнения вещей.
Необходимость в трех очередях и семафоре, чтобы все координировать, — не самое лучшее. Я пытаюсь быть слишком умным?
Просматривая некоторые из руководство libdispatch, особенно этот пост от сопровождающего, заставляет меня задуматься, является ли весь этот вариант использования чем-то вроде анти-паттерна (особенно в отношении использования семафоров для ожидания асинхронной работы). Это просто плохая идея — попытаться переоборудовать такую функциональность в GCD?
Учитывая мои первоначальные требования (т.е. ограниченная пропускная способность и настраиваемый порядок планирования), разумен ли этот подход удаленно? Если да, то как это можно улучшить?
Заранее спасибо!
final class Queue {
let count: Int
private let block: (Range<Int>) -> Int
private let semaphore: DispatchSemaphore
private let objectQueue: DispatchQueue
private let semaphoreQueue: DispatchQueue
private let workItemQueue: DispatchQueue
private var workItems = Array<DispatchWorkItem>()
init(count: Int, qos: DispatchQoS = .default, block: @escaping (Range<Int>) -> Int) {
self.count = count
self.block = block
self.semaphore = .init(value: count)
self.objectQueue = .init(label: "object-queue", qos: qos)
self.semaphoreQueue = .init(label: "semaphore-queue", qos: qos)
self.workItemQueue = .init(label: "workitem-queue", qos: qos, attributes: [.concurrent])
}
}
extension Queue {
func async(execute work: @escaping () -> ()) {
objectQueue.async { [self] in
let workItem = DispatchWorkItem(flags: [.inheritQoS]) {
work()
semaphore.signal()
}
workItems.append(workItem)
semaphoreQueue.async {
semaphore.wait()
objectQueue.async {
let index = block(workItems.indices)
let workItem = workItems.remove(at: index)
workItemQueue.async(execute: workItem)
}
}
}
}
func async(execute workItem: DispatchWorkItem) {
async { workItem.perform() }
}
}
Редактировать
В block
закрытие выбирает индекс следующего запланированного рабочего элемента. Его аргумент Range
гарантированно не будет пустым.
Рабочие элементы находятся в том же порядке, в котором они были поставлены в очередь. block
не предназначен для связи с конкретными рабочими элементами, а скорее для принятия решения между выбором тех, которые некоторое время ожидают выполнения, и тех, которые только что поступили в очередь.
Я использую его таким образом:
let count = max(1, ProcessInfo.processInfo.activeProcessorCount - 2)
let queue = Queue(count: count) { indices in
let value = sqrt(.random(in: 0 ..< 1))
let index = Int(value * Double(indices.count))
return indices[index]
}
В этой конфигурации очередь выбирает рабочие элементы случайным образом, перекос в сторону более новых. Это может показаться нелогичным, но при работе с большим количеством дорогостоящих задач, которые могут быть отменены (в моем случае загрузка ресурсов для отображения в представлении коллекции во время прокрутки), это приводит к заметному повышению производительности.