Как реализовать синхронизацию между потоками без блокировок в Python
Что проверяют
Понимаете ли вы, что в Python «без блокировок» на практике означает «без ручных Lock», а не настоящие lock-free алгоритмы, и знаете ли, что на самом деле даёт GIL. Типичное продолжение: безопасен ли counter += 1 из нескольких потоков?
Как отвечать
Каркас: убрать общее изменяемое состояние, а там, где обмен нужен, передавать данные сообщениями.
queue.Queue: потокобезопасная очередь для схемы производитель и потребитель. Внутри у неё есть блокировки и условные переменные, просто управляете ими не вы. Для остановки потребителей шлют сигнальное значение, напримерNone, по одному на каждый поток;threading.Event: флаг «можно продолжать», который один поток выставляет, а другие ждут черезwait();concurrent.futures: каждая задача получает свои входные данные и возвращает результат черезFuture, собирать его можно в главном потоке. Общего состояния нет вовсе;threading.local()иcontextvars: у каждого потока или задачи своя копия данных;- неизменяемые структуры: кортежи и
frozensetможно читать из любого числа потоков.
Отдельно про GIL. В CPython отдельные операции над встроенными типами, например list.append или deque.popleft, выполняются атомарно. Но counter += 1 это чтение, сложение и запись, между ними поток может смениться, и инкременты теряются. Примитива compare-and-swap в стандартной библиотеке нет, поэтому настоящий lock-free код на чистом Python не пишут.
Хорошо упомянуть сборку без GIL: в 3.13 она появилась как экспериментальная, в 3.14 стала официально поддерживаемой, но пока ставится отдельно. Код, который полагался на «атомарность благодаря GIL», там может сломаться.
Для ввода-вывода есть ещё asyncio: один поток, и между двумя await корутину никто не прервёт.
Где спотыкаются
Утверждают, что GIL делает любой код потокобезопасным. Он защищает внутренности интерпретатора, а не инварианты вашей программы.
Называют Queue lock-free. Честнее сказать, что блокировки спрятаны внутри библиотеки.
Запускают несколько потребителей и отправляют одно сигнальное значение. Один поток выходит, остальные висят на get() навсегда, и join() не возвращается.