Что такое гонка потоков
Что проверяют
Понимаете ли вы, откуда в приложении берётся параллелизм, и знаете ли инструменты платформы. Вопрос почти всегда продолжается практикой: обновление интерфейса из фонового потока, общий кэш, состояние после отмены запроса.
Как отвечать
Стоит сразу развести два понятия. Гонка данных это неупорядоченный доступ к одной памяти, где хотя бы одно обращение на запись, и это неопределённое поведение. Гонка состояний шире: результат зависит от порядка событий и может оказаться неверным даже там, где каждая отдельная операция безопасна.
Инструменты, от дешёвых к дорогим:
- изоляция: состояние принадлежит одному актору или одной последовательной очереди, наружу отдаются значимые типы;
actorи@MainActorиз Swift Concurrency плюс проверкаSendable, которая переносит часть ошибок в компиляцию;- конкурентная очередь с барьером, когда чтений много, а записей мало;
os_unfair_lockилиNSLockтам, где нужна точечная блокировка.
let queue = DispatchQueue(label: "cache", attributes: .concurrent)
queue.async(flags: .barrier) { storage[key] = value }
let cached = queue.sync { storage[key] }Отдельно про правило главного потока: интерфейс трогается только с него, за этим следит Main Thread Checker, а сами гонки в рантайме ловит Thread Sanitizer.
Где спотыкаются
Ставят блокировку на запись, а чтение оставляют свободным либо в разных методах запирают разные объекты. Защищено не поле, а весь доступ к нему, и правило должно соблюдаться в каждой точке.
Считают, что актор снимает вопрос целиком. Актор реентерабелен: на каждом await метод уступает исполнение, состояние за это время меняется, поэтому проверку и действие нельзя разносить через ожидание. Ссылочный тип, выпущенный за границу актора, снова доступен всем.
Списывают редкий сбой на плавающий баг и добавляют задержку вместо синхронизации. На устройстве пользователя гонка проявится при другой нагрузке и другой версии системы, а искать её надо санитайзером на нагрузочном сценарии.