Что такое гонка данных и в каких системах она возникает
Что проверяют
Различаете ли вы гонку данных и гонку состояний и понимаете ли, что GIL от них не спасает. Для питониста это ещё и вопрос про модель исполнения: потоки, процессы и событийный цикл живут по разным правилам, и ошибки у них разные.
Как отвечать
Гонка данных это одновременный доступ к общему состоянию из нескольких потоков, где хотя бы одно обращение на запись, без синхронизации. Результат зависит от планировщика и потому невоспроизводим.
GIL сериализует выполнение байткода, но не операции уровня языка. x += 1 это чтение, сложение и запись, переключение интерпретатора может произойти между ними, и счётчик теряет инкременты. Одиночные операции вроде list.append атомарны, а составные вроде d[k] = d[k] + 1 уже нет. В сборке без GIL, которая появилась в новых версиях CPython, исчезает и эта половинчатая защита.
Где встречается:
- потоки: общие словари и кэши, ленивая инициализация, счётчики. Лечится
threading.Lock,queue.Queue, данными, локальными для потока; - asyncio: поток один, но
awaitэто точка переключения, и проверка с последующим изменением через неё разъезжается. Нуженasyncio.Lock, а неthreading.Lock, который блокирует цикл целиком; - процессы и воркеры: общей памяти нет, гонка переезжает в базу или кэш и превращается в потерянное обновление.
Где спотыкаются
Считают GIL гарантией потокобезопасности. Он защищает внутренние структуры интерпретатора от разрушения, а бизнес-инвариант защищает разработчик.
Берут блокировку в одном месте и забывают в другом либо создают новый объект блокировки на каждый вызов. Блокировка защищает не данные, а соглашение: она работает, только если её берут все, кто трогает это состояние.
Переносят потоковые решения на распределённый случай. threading.Lock не виден другому процессу и другому экземпляру приложения, поэтому read-modify-write над строкой в базе чинят транзакцией, блокировкой строки или атомарной операцией на стороне хранилища.