> Как сделать потокобезопасным общий массив при синхронных операциях в concurrent очереди (iOS, Swift)
Уровень: senior · Роль: mobile · Категория: Технические вопросы
Компании: Московская биржа
Стек: iOS, Swift
> Пример ответа
Короткий ответ
Потокобезопасность общего массива при синхронных операциях в concurrent очереди достигается через барьеры (barrier). Запись выполняется с флагом .barrier, чтение - обычной синхронной задачей. Это гарантирует эксклюзивный доступ на запись и параллельный доступ на чтение. Альтернативы - отдельная serial очередь, lock или атомарные свойства, но barrier - наиболее эффективный паттерн для read-heavy сценариев.
Подробное объяснение
Concurrent очередь сама по себе не обеспечивает безопасность данных - она лишь управляет порядком выполнения задач. Если несколько потоков одновременно пишут в массив, возникает data race. Решение - использовать DispatchQueue с атрибутом .concurrent и методы sync/async с барьером:
- Запись:
queue.async(flags: .barrier) { array.append(item) }- задача с барьером блокирует очередь, пока не завершится, и не запускается, пока выполняются другие задачи. Это даёт эксклюзивный доступ. - Чтение:
queue.sync { return array[index] }- обычная задача, может выполняться параллельно с другими чтениями, но не с записью.
Барьер гарантирует: пока идёт запись, ни одно чтение не выполняется, и наоборот. При этом чтения не блокируют друг друга, что даёт выигрыш в производительности по сравнению с serial очередью.
Важно: сам массив должен быть var, а не let, и доступ к нему - только внутри очереди. Никаких прямых обращений извне.
На практике
Типичный пример - потокобезопасная обёртка над массивом для кэша или хранилища данных. Создаётся класс с приватной очередью и приватным массивом, наружу выходят методы read и write. Для синхронных операций используется sync на запись, если нужно вернуть результат, или async с барьером для fire-and-forget.
Для чтения предпочтителен sync, так как он возвращает значение немедленно и не требует обработки completion. Для записи - async с барьером, чтобы не блокировать вызывающий поток, если результат не нужен.
Если операция чтения должна вернуть копию массива - используйте return array внутри sync, это безопасно, так как копирование происходит под защитой очереди.
Пример кода
SWIFTfinal class ThreadSafeArray<Element> {private var array: [Element] = []private let queue = DispatchQueue(label: "com.example.array", attributes: .concurrent)func append(_ element: Element) {queue.async(flags: .barrier) {self.array.append(element)}}func element(at index: Int) -> Element? {queue.sync {guard index >= 0 && index < array.count else { return nil }return array[index]}}var count: Int {queue.sync { array.count }}func removeAll() {queue.async(flags: .barrier) {self.array.removeAll()}}}
Использование:
SWIFTlet safeArray = ThreadSafeArray<Int>()safeArray.append(1)safeArray.append(2)if let value = safeArray.element(at: 0) {print(value) // 1}
Как отвечать на собеседовании
Начните с проблемы: concurrent очередь не защищает данные, нужен механизм синхронизации. Затем объясните паттерн barrier: записи - с флагом .barrier, чтения - обычные sync. Подчеркните trade-off: барьер замедляет запись, но ускоряет чтение. Упомяните альтернативы (serial queue, NSLock, атомарные свойства) и объясните, почему barrier предпочтительнее для read-heavy сценариев.
Если спросят про async vs sync - поясните: sync для чтения обязателен, чтобы вернуть значение; для записи можно async, если не нужен результат. Добавьте, что sync на concurrent очереди с барьером не вызывает deadlock, если вызывается не из той же очереди.
Что проверяет интервьюер
- Понимание разницы между concurrent и serial очередями.
- Знание механизма
barrierи его семантики. - Умение проектировать потокобезопасные абстракции.
- Осознание trade-off между производительностью и безопасностью.
- Внимание к деталям: приватность массива, отсутствие прямого доступа, корректная обработка границ.
Типичные ошибки
- Использование
syncдля записи - блокирует вызывающий поток без необходимости. - Забытый
.barrier- тогда запись не эксклюзивна, data race остаётся. - Прямой доступ к массиву извне очереди - ломает инкапсуляцию.
- Использование
letдля массива - нельзя изменить, только заменить, что тоже требует барьера. - Вызов
syncиз той же очереди - deadlock, если очередь serial; для concurrent с barrier - тоже риск. - Игнорирование копирования при чтении - если вернуть ссылку на массив, внешний код сможет мутировать его без барьера.
> Похожие задачи по mobile
Как реализовать функцию умножения числа на десять с обработкой символов
Что такое сайд таблица (side table) в контексте weak ссылок и как она работает
Как написать структуру или класс для бинарного дерева поиска
Всегда ли структуры хранятся в стеке
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью