Какие проблемы встречаются в многопоточном коде и как их решать
Что проверяют
Можете ли вы назвать конкретные проблемы и привязать к ним инструменты iOS: GCD, блокировки и Swift Concurrency. Часто следом дают кусок кода с DispatchQueue.main.sync или актором и просят найти ошибку.
Как отвечать
Пройдитесь по проблемам и сразу называйте решение:
- гонка данных: два потока пишут в один
ArrayилиDictionaryбез синхронизации. Коллекции Swift не потокобезопасны, итог это порча памяти и падения в случайных местах. Решения: последовательная очередь,NSLockилиOSAllocatedUnfairLock, в новом кодеactor; - логическая гонка: даже при защищённых чтениях операция «проверить и записать» из двух потоков ломает инвариант. Защищать нужно всю операцию целиком;
- deadlock: типичный случай это
DispatchQueue.main.syncиз главного потока илиsyncна ту же последовательную очередь, в которой код уже выполняется. Ещё вариант: два замка, захваченные в разном порядке; - UI не с главного потока: UIKit требует главного потока, Main Thread Checker в Xcode ловит нарушения при запуске. В Swift Concurrency такой код помечают
@MainActor; - thread explosion: сотни блокирующих задач на concurrent-очереди заставляют GCD плодить потоки. Swift Concurrency держит пул по числу ядер, поэтому блокировать поток внутри
async-функции семафором илиsleepнельзя; - реентерабельность акторов: после каждого
awaitвнутри актора состояние могло измениться, проверку нужно повторять; - инверсия приоритетов: задача с высоким QoS ждёт замок, который держит фоновая.
Диагностика: Thread Sanitizer в схеме Xcode, Main Thread Checker, а в Swift 6 строгая проверка конкурентности, где многие гонки ловит компилятор через Sendable.
Где спотыкаются
Путают data race и race condition. Первое это одновременный доступ к памяти без синхронизации, второе это ошибка логики из-за порядка выполнения, и она возможна без первого.
Считают, что актор решает всё. Реентерабельность и блокирующие вызовы внутри async-функций дают ошибки, которых компилятор не видит.
Лечат любую проблему обёрткой в DispatchQueue.main.async. Это прячет гонку за задержкой, а не устраняет её, и нагружает главный поток.