Какие опасности существуют при работе с потоками
Что проверяют
Понимаете ли вы, что GIL не делает код потокобезопасным, и чем потоки отличаются от процессов и asyncio. Следом обычно спрашивают, почему counter += 1 в двух потоках теряет инкременты, если GIL есть.
Как отвечать
Начните с CPython: GIL даёт выполнять байт-код одному потоку за раз, поэтому потоки ускоряют ввод-вывод, но не счётные задачи. Для CPU берут ProcessPoolExecutor. В 3.13 появилась экспериментальная сборка без GIL, в 3.14 она стала официально поддерживаемой, но по умолчанию GIL на месте. Опасности:
- гонки:
counter += 1это чтение, сложение и запись, и поток может переключиться между ними. Отдельныйlist.appendв CPython атомарен, а составное «проверить и изменить» нет. Лечитсяthreading.Lockвwith lock:; - взаимные блокировки: два потока берут два замка в разном порядке. Защита: единый порядок захвата,
acquire(timeout=...), минимум кода под замком. Повторный захватLockтем же потоком тоже вешает его, для этого естьRLock; - потерянные исключения: ошибка в потоке не долетает до основного, по умолчанию
threading.excepthookлишь печатает её в stderr. ВThreadPoolExecutorисключение лежит вFutureдо вызоваresult(); - поток не убить снаружи: останавливают кооперативно через
threading.Event, а daemon-потоки при выходе обрываются безfinally; - смешивание с asyncio: блокирующий вызов в корутине стопорит весь цикл событий. Его выносят через
asyncio.to_thread, а из потока в цикл возвращаются черезloop.call_soon_threadsafe; - fork при живых потоках: дочерний процесс наследует захваченный кем-то замок и зависает. С 3.12
os.fork()в многопоточном процессе даётDeprecationWarning.
Для обмена данными безопаснее всего queue.Queue, синхронизация в ней уже есть.
Где спотыкаются
Говорят, что из-за GIL гонок в Python не бывает. GIL защищает внутренности интерпретатора, а не ваши инварианты.
Берут потоки для тяжёлых вычислений и удивляются, что четыре потока работают не быстрее одного.
Отправляют задачи через executor.submit, не вызывают result(), и ошибки не видны нигде.