Почему важно не блокировать основной поток при выполнении тяжелых операций в Python
Что проверяют
Различаете ли вы задачи, упирающиеся в процессор, и задачи, ждущие ввод-вывод, и знаете ли, что делает GIL. Дальше вопрос почти всегда переходит в практическое «а как тогда правильно».
Как отвечать
В CPython байткод в каждый момент исполняет один поток, его держит глобальная блокировка интерпретатора. На операциях ввода-вывода блокировка отпускается, поэтому ожидание сети и диска потоками распараллеливается нормально. Чистый расчёт её не отпускает, и второй поток просто стоит в очереди.
Отсюда выбор инструмента:
- ждём внешнюю систему:
asyncioлибо пул потоков; - считаем сами: отдельные процессы,
multiprocessingилиProcessPoolExecutor; - тяжёлая библиотека на C часто отпускает блокировку внутри своих операций, тогда и потоки дают выигрыш.
Отдельная ловушка асинхронного кода: блокирующий вызов внутри корутины останавливает не свою задачу, а весь цикл событий, то есть все задачи разом. Синхронный клиент базы или time.sleep в обработчике превращает асинхронный сервис в последовательный.
result = await asyncio.to_thread(blocking_call, arg)В вебе долгую работу вообще выносят из обработки запроса в очередь задач, а клиенту сразу отдают идентификатор и способ узнать статус.
Где спотыкаются
Берут threading для вычислений и удивляются, что стало медленнее. Потоки борются за одну блокировку и добавляют переключения контекста, поэтому расчётная задача в два потока обычно чуть проигрывает однопоточной.
Путают поток и цикл событий. Фраза «запустил в фоне» через создание задачи ничего не меняет, если сама задача синхронная: она выполняется в том же цикле и держит его до конца. В фон её уводит только поток или процесс.
Ломают графический интерфейс. Вынести расчёт в поток правильно, но обновлять виджеты из рабочего потока нельзя ни в одном популярном тулките: результат возвращают в главный поток через очередь или штатный механизм самого тулкита.