Многопоточность и GCD
Dispatch-очереди, QoS, взаимоблокировки, барьеры, группы и семафоры, `Operation` и гонки данных, которые эти средства призваны предотвращать.
13 вопросов
MiddleТеорияОчень частоРазберите четыре сочетания последовательной/параллельной очереди с sync/async — что делает каждое?
Разберите четыре сочетания последовательной/параллельной очереди с sync/async — что делает каждое?
async возвращается сразу; sync блокирует вызывающего до завершения блока. Последовательная очередь выполняет блоки по одному по порядку; параллельная — много сразу. serial+async — упорядоченная работа, concurrent+async — параллельная, а sync на свою последовательную очередь даёт взаимоблокировку.
Типичные ошибки
- ✗Меняют местами смысл
sync(блокирует) иasync(возвращается сразу) - ✗Считают, что параллельная очередь сохраняет порядок своих блоков
- ✗Упускают, что
syncна текущую последовательную очередь даёт взаимоблокировку
Уточняющие вопросы
- →Почему concurrent+async не гарантирует порядок между блоками?
- →Что именно блокируется при вызове
syncна очередь, на которой вы находитесь?
MiddleДебаггингОчень частоView обновляется с фоновой очереди и падает время от времени — почему и как ловить это в CI?
View обновляется с фоновой очереди и падает время от времени — почему и как ловить это в CI?
UIKit не потокобезопасен — его view рассчитаны на главный поток. Обращение к imageView вне главного портит состояние, поэтому падает лишь иногда. Исправление — переход на DispatchQueue.main.async. В CI Main Thread Checker валит прогон.
Типичные ошибки
- ✗Считают UIKit потокобезопасным и что любая очередь может трогать view
- ✗Винят force-unwrap вместо доступа к UI вне главного потока
- ✗Берут
main.syncтам, где правильный переход —main.async
Уточняющие вопросы
- →Почему доступ к UIKit вне главного падает лишь иногда, а не всегда?
- →Что именно инструментирует Main Thread Checker, чтобы это обнаружить?
JuniorТеорияЧастоЧто такое гонка данных и как последовательная очередь или блокировка защищают общее изменяемое состояние?
Что такое гонка данных и как последовательная очередь или блокировка защищают общее изменяемое состояние?
Гонка данных — это когда два потока обращаются к одной изменяемой памяти, хотя бы один пишет, без синхронизации — поведение не определено. Предотвращают её сериализацией — пропуская каждое чтение и запись через одну последовательную DispatchQueue или блокировку.
Типичные ошибки
- ✗Считают гонкой данных любую многопоточность, даже над неразделяемыми данными
- ✗Думают, что последовательная очередь — только про порядок, а не про эксклюзивный доступ
- ✗Полагают, что чтения безопасны без синхронизации, пока пишет один поток
Уточняющие вопросы
- →Почему несинхронизированное чтение рядом с записью всё равно гонка данных?
- →Как последовательная очередь даёт эксклюзивный доступ без явной блокировки?
JuniorТеорияЧастоОчереди Grand Central Dispatch против OperationQueue — что Operation добавляет к обычному dispatch-блоку?
Очереди Grand Central Dispatch против OperationQueue — что Operation добавляет к обычному dispatch-блоку?
Operation — переиспользуемый объект, а не одноразовое замыкание. Он добавляет зависимости задач, отмену через isCancelled, лимит параллелизма и наблюдаемое состояние — у голого GCD-блока этого нет. OperationQueue работает поверх GCD, добавляя жизненный цикл.
Типичные ошибки
- ✗Думают, что
OperationQueueобходит GCD, а не построен поверх него - ✗Считают, что блок
DispatchQueue.asyncможно отменить уже во время выполнения - ✗Полагают, что зависимости между GCD-блоками так же просты, как у
Operation
Уточняющие вопросы
- →Как выразить, что операция B должна ждать операцию A?
- →Когда обычная GCD-очередь лучше, чем
OperationQueue?
MiddleКодЧастоСделайте общий кэш потокобезопасным через параллельную очередь и барьерную запись
Сделайте общий кэш потокобезопасным через параллельную очередь и барьерную запись
Возьмите приватную параллельную DispatchQueue. Чтения через queue.sync идут параллельно; записи — через queue.async(flags: .barrier), одни. Обычный sync даёт записи перекрыться с чтениями — эксклюзивной её делает .barrier.
Типичные ошибки
- ✗Думают, что обычное чтение
syncделает параллельные записи эксклюзивными - ✗Ставят
.barrierна чтения, бессмысленно их сериализуя - ✗Считают, что один FIFO-порядок делает параллельный доступ безопасным
Уточняющие вопросы
- →Почему чтения должны использовать
sync, а неasync, чтобы вернуть значение? - →Что происходит с чтениями, поставленными после барьерной записи?
MiddleДизайнЧастоВы строите конвейер загрузки изображений для прокручиваемой ленты. Каждой ячейке нужно изображение, скачанное по сети, затем декодированное и уменьшенное вне главного потока. Когда ячейка уходит с экрана или переиспользуется, её незавершённая загрузка должна отменяться, чтобы работа не тратилась впустую и устаревшее изображение не попало в переиспользованную ячейку. Некоторые загрузки зависят от общего шага предзагрузки. Спроектируйте конвейер — смоделируйте каждую загрузку как единицу работы, свяжите стадии fetch, decode и resize, отменяйте при переиспользовании, ограничьте параллелизм и доставьте изображение в нужную ячейку. Сравните OperationQueue с подклассами Operation — через isCancelled, addDependency и maxConcurrentOperationCount — с голым GCD-диспатчем и объясните, почему GCD усложняет отмену и зависимости.
Вы строите конвейер загрузки изображений для прокручиваемой ленты. Каждой ячейке нужно изображение, скачанное по сети, затем декодированное и уменьшенное вне главного потока. Когда ячейка уходит с экрана или переиспользуется, её незавершённая загрузка должна отменяться, чтобы работа не тратилась впустую и устаревшее изображение не попало в переиспользованную ячейку. Некоторые загрузки зависят от общего шага предзагрузки. Спроектируйте конвейер — смоделируйте каждую загрузку как единицу работы, свяжите стадии fetch, decode и resize, отменяйте при переиспользовании, ограничьте параллелизм и доставьте изображение в нужную ячейку. Сравните OperationQueue с подклассами Operation — через isCancelled, addDependency и maxConcurrentOperationCount — с голым GCD-диспатчем и объясните, почему GCD усложняет отмену и зависимости.
Смоделируйте каждую загрузку как Operation — цепочку fetch→decode→resize — на OperationQueue. Проверяйте isCancelled и отменяйте при переиспользовании, чтобы устаревшее изображение не попадало в ячейку. addDependency — предзапрос, maxConcurrentOperationCount — лимит. У GCD нет ни того, ни другого.
Типичные ошибки
- ✗Полагают, что GCD может отменить уже отправленную на очередь работу
- ✗Поздно перезаписывают изображение ячейки вместо отмены устаревшей загрузки
- ✗Полагаются на FIFO-порядок, чтобы выразить настоящую зависимость
Уточняющие вопросы
- →Как гарантировать, что завершённая загрузка обновит только свою исходную ячейку?
- →Где в цепочке fetch→decode→resize проверять
isCancelled?
MiddleТеорияЧастоГонка данных против состояния гонки — в чём разница и может ли быть одно без другого?
Гонка данных против состояния гонки — в чём разница и может ли быть одно без другого?
Гонка данных — несинхронизированный доступ к одной памяти с записью, неопределённое поведение. Состояние гонки — логический баг, зависящий от тайминга. Они независимы — одно бывает без другого, и им нужны разные исправления.
Типичные ошибки
- ✗Считают гонку данных и состояние гонки синонимами
- ✗Полагают, что устранение гонки данных само чинит логическую ошибку тайминга
- ✗Думают, что гонка данных безвредна, если вывод всё ещё выглядит верным
Уточняющие вопросы
- →Приведите состояние гонки только на атомарных операциях, без гонки данных.
- →Почему гонка данных — неопределённое поведение, даже если результат выглядит верным?
MiddleТеорияЧастоСреди примитивов синхронизации DispatchGroup, DispatchSemaphore и NSLock — для чего нужен каждый?
Среди примитивов синхронизации DispatchGroup, DispatchSemaphore и NSLock — для чего нужен каждый?
DispatchGroup ждёт завершения набора async-задач. DispatchSemaphore ограничивает, сколько потоков сразу входят в участок. NSLock даёт одному потоку эксклюзивный доступ. Каждый — под свою нужду: завершение, ёмкость, эксклюзивность.
Типичные ошибки
- ✗Берут
DispatchSemaphoreтам, где нужна обычнаяNSLock - ✗Считают, что
DispatchGroupобеспечивает взаимное исключение - ✗Думают, что все три — просто разные имена мьютекса
Уточняющие вопросы
- →Как
DispatchGroupузнаёт, что последняя async-задача завершилась?
MiddleДебаггингЧастоDispatchQueue.main.sync с главного потока зависает — найдите и исправьте взаимоблокировку
DispatchQueue.main.sync с главного потока зависает — найдите и исправьте взаимоблокировку
main.sync блокирует вызывающий поток, пока блок не выполнится на главной очереди. Но вызывающий и есть главный поток, он заблокирован, поэтому блок не выполняется — взаимоблокировка. Исправление — если уже на главном, выполните напрямую; иначе main.async.
Типичные ошибки
- ✗Считают главную очередь параллельной, способной войти в себя
- ✗Винят
reloadData(), а не синхронный диспатч на главную очередь - ✗Оставляют
main.sync, лишь прикрывая его проверкой потока
Уточняющие вопросы
- →Почему
main.asyncизбегает взаимоблокировки, которую даёт здесьmain.sync? - →Когда вызов
syncс главного потока на другую очередь безопасен?
MiddleКодЧастоПредскажите вывод в консоль этих вложенных диспатчей на очереди
Предскажите вывод в консоль этих вложенных диспатчей на очереди
Порядок — A B D E C. work.sync блокирует главный поток и выполняет блок на месте, поэтому B и D идут до E. C поставлен через main.async, поэтому выполняется лишь после возврата главного потока в run loop — после E.
Типичные ошибки
- ✗Думают, что
main.asyncвыполняет блок на месте, а не на следующем проходе run loop - ✗Считают, что
work.syncвозвращается до завершения своего блока - ✗Полагают, что запланированный блок выполняется там, где записан в коде
Уточняющие вопросы
- →Почему
Cне может выполниться, пока главный поток внутриwork.sync? - →Изменится ли порядок, если заменить
work.syncнаwork.async?
MiddleДебаггингЧастоЛенивый синглтон инициализируется дважды под нагрузкой — почему и как это исправить?
Ленивый синглтон инициализируется дважды под нагрузкой — почему и как это исправить?
Проверка-и-присваивание не атомарны — два потока создают по Manager, поэтому init идёт дважды. Исправьте через static let shared = Manager() — Swift инициализирует его ровно раз, лениво и потокобезопасно. Блокировка или очередь тоже годятся.
Типичные ошибки
- ✗Считают проверку-и-присваивание у
static varатомарной между потоками - ✗Винят force-unwrap, а не неатомарную проверку
- ✗Добавляют
@MainActorвместоstatic letдля одноразовой инициализации
Уточняющие вопросы
- →Какую гарантию даёт Swift о моменте инициализации
static let? - →Как последовательная очередь сериализовала бы создание вместо этого?
MiddleТеорияИногдаЧто такое классы quality-of-service QoS, что такое инверсия приоритетов и как GCD её устраняет?
Что такое классы quality-of-service QoS, что такое инверсия приоритетов и как GCD её устраняет?
Класс QoS (.userInteractive….background) сообщает GCD важность задачи, задавая приоритет потока и расход энергии. Инверсия приоритетов — высокоприоритетная задача ждёт ресурс, удерживаемый низкоприоритетной. GCD устраняет её донорством — поднимая удерживающего до QoS ожидающего.
Типичные ошибки
- ✗Считают
QoSкосметической меткой без влияния на планирование - ✗Путают инверсию приоритетов с тем, что у задачи просто низкий приоритет
- ✗Думают, что GCD решает инверсию убийством или переупорядочиванием очередей
Уточняющие вопросы
- →Какой
QoSвыберете для сетевого предзапроса и для обработки нажатия? - →Почему донорство приоритета нацелено на удерживающего, а не на ожидающего?
SeniorПроизводительностьРедкоТроттлинг на счётном примитиве DispatchSemaphore вызывает взрывной рост потоков — продиагностируйте и переработайте его
Троттлинг на счётном примитиве DispatchSemaphore вызывает взрывной рост потоков — продиагностируйте и переработайте его
Каждый заблокированный wait() паркует поток GCD; не видя простаивающих, GCD плодит новые — десятки застревают на семафоре (взрыв потоков), тратя память. Переработка без блокировки потоков — лимит через maxConcurrentOperationCount или async/await.
Типичные ошибки
- ✗Считают, что заблокированный поток ничего не стоит держать
- ✗Думают, что меньшее значение семафора мешает GCD плодить потоки
- ✗Меняют блокирующий
wait()на блокирующийsync, что тоже паркует потоки
Уточняющие вопросы
- →Почему GCD создаёт новые потоки, когда существующие заблокированы?
- →Как async/await ограничивает параллелизм, не паркуя поток?