Как избежать блокировки при чтении большого файла в синхронном приложении?
Что проверяют
Понимаете ли вы, какая именно блокировка имеется в виду: съеденная память или занятый поток, который должен отвечать пользователю или другим запросам. Следом почти всегда спрашивают, почему для чтения файла не подходит asyncio сам по себе и чем здесь мешает GIL.
Как отвечать
Разведите две проблемы и дайте решение для каждой:
- память: не читать файл целиком через
read()илиreadlines(). Объект файла итерируется построчно с буферизацией, а бинарные данные читаются блокамиf.read(1 << 20)в цикле с:=. Обработку оформляйте генератором, тогда в памяти живёт только текущий кусок. Для случайного доступа к огромному файлу естьmmap; - поток: вынести чтение в
concurrent.futures.ThreadPoolExecutorи получить результат черезFutureили колбэк. Системный вызов чтения отпускает GIL, поэтому для ввода-вывода потоки работают нормально; - тяжёлый разбор: если после чтения идёт парсинг или вычисления, это уже нагрузка на CPU, и потоки упрутся в GIL. Здесь нужен
ProcessPoolExecutorили вынос в фоновую очередь задач вроде Celery; - если рядом всё же есть event loop:
asyncio.to_thread()илиloop.run_in_executor(). Библиотекаaiofilesделает то же самое, просто прячет пул потоков за асинхронным интерфейсом.
Хорошо прозвучит объяснение, почему файл не бывает по-настоящему асинхронным: обычный файл для select и epoll всегда готов к чтению, поэтому event loop не может ждать его так же, как сокет, и чтение всё равно уходит в поток.
Где спотыкаются
Запускают поток и тут же вызывают join(). Главный поток стоит ровно так же, как без потока, только кода стало больше.
Предлагают aiofiles в синхронном приложении. Ради одного файла переписывать программу на async бессмысленно, пул потоков решает задачу напрямую.
Обещают ускорить разбор большого CSV потоками. Из-за GIL в стандартной сборке CPython чистый Python в нескольких потоках не выполняется параллельно, для CPU нужны процессы или векторизация в pandas с чтением через chunksize.