Вопрос проверяет понимание конкурентного доступа к данным, особенностей DispatchQueue и умение выбирать подходящий механизм синхронизации для сценариев «много чтений — мало записей».
barrier в GCD — это специальная задача, которая на concurrent queue выполняется эксклюзивно: она ждёт завершения всех ранее запланированных задач и блокирует выполнение последующих, пока сама не закончится. Это удобно для потокобезопасной записи при параллельных чтениях. Обычно barrier используют в структурах вроде кешей или хранилищ, где чтений много, а записей мало. На serial queue barrier не даёт преимуществ, потому что там и так всё выполняется по очереди.
DispatchBarrier — это способ построить поведение, похожее на «read-write lock», но средствами GCD. Он особенно полезен, когда хочется разрешить параллельные чтения и при этом обеспечить эксклюзивную запись.
Определение: barrier — это задача, которая на concurrent queue выполняется только тогда, когда завершились все задачи перед ней, и во время её выполнения никакие другие задачи из этой очереди не выполняются.
Важно: это правило относится именно к одной конкретной очереди.
Представим очередь и задачи в ней:
read A (обычная async)
read B (обычная async)
write X (async(flags: .barrier))
read C (обычная async)
Поведение будет таким:
read A и read B могут выполняться параллельно.
write X начнётся только после завершения read A и read B.
Пока выполняется write X, read C не начнётся.
После завершения write X чтения снова могут выполняться параллельно.
То есть barrier создаёт «точку эксклюзивности» для записи.
В многопоточном коде популярный паттерн — «много чтений, мало записей».
Например:
кеш картинок
словарь с результатами вычислений
локальный in-memory storage
Если сделать всё через serial queue, то:
и чтения, и записи будут строго по очереди
это просто, но может стать узким местом
С barrier можно:
разрешить параллельные чтения
обеспечить безопасные записи
Ниже пример обёртки, где чтения параллельные, а записи эксклюзивные:
final class ThreadSafeCache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
private let queue = DispatchQueue(label: "cache.queue", attributes: .concurrent)
func get(_ key: Key) -> Value? {
queue.sync {
storage[key]
}
}
func set(_ value: Value, for key: Key) {
queue.async(flags: .barrier) {
self.storage[key] = value
}
}
func remove(_ key: Key) {
queue.async(flags: .barrier) {
self.storage.removeValue(forKey: key)
}
}
}
get через syncТут важно разделить два понятия:
параллельность выполнения очереди (concurrent queue)
тип вызова (sync / async)
get обычно нужен «прямо сейчас», поэтому делаем sync, чтобы вернуть значение сразу. При этом чтения из concurrent queue могут выполняться параллельно (если несколько потоков одновременно вызывают get).
async(flags: .barrier)Запись можно делать асинхронно:
она защищена barrier
она не блокирует вызывающий поток
порядок записей относительно других задач очереди сохраняется
Если нужно, чтобы set завершился до продолжения кода, можно сделать queue.sync(flags: .barrier), но это увеличит риск блокировок.
barrier гарантирует эксклюзивность только относительно задач, поставленных в эту же очередь.
Если кто-то меняет storage напрямую или через другую очередь — защита ломается. Поэтому:
состояние должно быть инкапсулировано
доступ к нему должен идти только через одну очередь
На DispatchQueue.global() вы не контролируете, что ещё туда ставит система и другие части программы. Поэтому для корректного сценария «read/write» почти всегда создают собственную concurrent queue.
Если состояние лучше вообще не шарить — лучше не шарить.
Например, иногда вместо shared dictionary лучше:
передавать данные по копии (value semantics)
использовать actor (если проект на Swift Concurrency)
выносить работу в отдельный слой, который сериализует доступ
syncСамая частая ошибка — вызвать queue.sync внутри задачи, которая уже выполняется на queue. Это приведёт к взаимной блокировке.
Плохой пример (упрощённо):
queue.async {
let x = queue.sync { storage["a"] } // риск deadlock
}
Правило:
не вызывать sync на той же очереди, на которой вы уже находитесь
Используйте barrier, если выполняются условия:
Есть общее состояние (например, словарь/массив в памяти).
Очень много операций чтения.
Записи редкие, но должны быть безопасными.
Вы хотите повысить параллелизм по сравнению с serial queue.
Типичные кейсы:
кеши
хранилища конфигурации
карты соответствий / справочники
дедупликация запросов (храните in-flight операции и очищаете по завершению)
Есть сценарии, где лучше выбрать другой подход:
Записей много и они «тяжёлые»
barrier будет часто блокировать чтения, выгода исчезнет
Нужно сложное ожидание/координация задач
проще использовать OperationQueue или async/await
Есть риск обратиться к данным «мимо очереди»
лучше serial queue или actor, чтобы проще гарантировать безопасность
DispatchBarrier — хороший инструмент для паттерна «параллельные чтения + эксклюзивные записи» на собственной concurrent queue. Он даёт больше производительности, чем serial queue, но требует дисциплины: всё состояние должно быть скрыто и доступно только через эту очередь.