Какая эффективная структура данных для передачи информации или событий между тредами
Что проверяют
Знаете ли вы стандартные потокобезопасные примитивы Python и понимаете ли, почему самодельный список с блокировкой хуже. Следом обычно спрашивают, как корректно остановить воркеры и как передать данные из потока в asyncio.
Как отвечать
Базовый ответ: queue.Queue. Внутри у неё блокировка и условные переменные, поэтому ручная синхронизация не нужна. Что стоит назвать:
put()иget(): по умолчанию блокируют поток, естьtimeoutи неблокирующиеput_nowait()/get_nowait();maxsize: ограничение размера даёт обратное давление, быстрый производитель ждёт медленного потребителя, а не съедает память;task_done()иjoin(): позволяют дождаться, пока все задачи обработаны;LifoQueueиPriorityQueue: тот же интерфейс, другой порядок выдачи;queue.SimpleQueue: проще и быстрее, но безmaxsizeи учёта задач.
Для сигнала без данных подходит threading.Event, для сложных условий ожидания threading.Condition. Остановку делают сигнальным объектом (sentinel), по одному на воркер. С Python 3.13 есть Queue.shutdown(): после него get() у опустевшей очереди поднимает queue.ShutDown.
Отдельно про границы. Между процессами нужна multiprocessing.Queue, а asyncio.Queue не потокобезопасна: из обычного потока в цикл событий данные передают через loop.call_soon_threadsafe() или asyncio.run_coroutine_threadsafe(). Если нужен результат конкретной задачи, а не поток событий, проще взять ThreadPoolExecutor и Future.
Где спотыкаются
Опираются на GIL как на синхронизацию: «list.append же атомарный». Для связки «проверил, потом сделал» это не работает, а сборка без GIL, появившаяся в 3.13, делает такие допущения ещё опаснее.
Принимают решения по qsize() или empty(). Между проверкой и вызовом get() другой поток успеет забрать элемент, поэтому правильно ждать с таймаутом или ловить queue.Empty.
Забывают про остановку: воркеры с while True без sentinel не завершаются, а join() зависает навсегда, если кто-то не вызвал task_done() после обработки.