При запуске с методом spawn этот модуль создаёт дочерний процесс рекурсивно. Какую роль играет проверка __name__ == "__main__"?
import asyncio
import multiprocessing as mp
ctx = mp.get_context("spawn")
def worker():
asyncio.run(asyncio.sleep(0))
async def main():
process = ctx.Process(target=worker)
process.start()
process.join()
asyncio.run(main())
При spawn дочерний процесс запускает новый интерпретатор и импортирует исходный модуль. Поэтому без проверки if __name__ == "__main__": дочерний процесс снова выполнит asyncio.run(main()), создаст ещё один процесс и завершится с ошибкой о попытке создать процесс во время начальной загрузки.
Проверка ограничивает запуск управляющего кода только исходным процессом. Саму функцию worker дочерний процесс сможет импортировать и выполнить как целевую функцию.
Модель spawn появилась как переносимый способ создания процессов, не полагающийся на копирование адресного пространства родителя. Новый процесс начинает работу с чистого интерпретатора и загружает необходимые объекты через импорт модуля и сериализацию.
Такой подход безопаснее для программ с потоками и сложным состоянием интерпретатора, но требует отделять определения функций и классов от кода, который должен выполняться только один раз при запуске приложения.
Код верхнего уровня модуля выполняется при его импорте. В приведённом примере вызов asyncio.run(main()) находится на верхнем уровне, поэтому он выполняется не только в родительском процессе, но и при импорте модуля дочерним процессом.
Дочерний процесс начинает создавать собственного потомка ещё до завершения процедуры запуска. Это приводит к ошибке вида RuntimeError о безопасном импорте основного модуля; кроме того, приложение может получить неожиданные побочные эффекты от повторного выполнения кода инициализации.
Исправленный вариант помещает точку входа под защиту:
При импорте дочерним процессом выполняются определения worker и main, а условие __name__ == "__main__" оказывается ложным. Поэтому новый процесс не запускает main повторно; он использует ранее переданную функцию worker.
Вызов process.join() синхронный и блокирует поток event loop. В примере он вынесен в asyncio.to_thread, чтобы ожидание завершения процесса не блокировало другие корутины. Если в этот момент event loop не обслуживает параллельные задачи, обычный join() функционально сработает, но это плохая общая практика для асинхронного сервиса.
Функция, передаваемая процессу, должна быть доступна по импортируемому имени. Локальная функция, лямбда или замыкание обычно не подходят для spawn, поскольку целевой объект нужно сериализовать и восстановить в новом интерпретаторе.
На системах, где используется fork, модуль обычно не импортируется заново таким способом, однако полагаться на это нельзя: выбор метода зависит от платформы и настроек приложения. Защита точки входа необходима для переносимого кода, использующего multiprocessing или ProcessPoolExecutor.
В асинхронном HTTP-сервисе нужно вынести CPU-bound обработку в дочерние процессы. Разработчик запускает пул на уровне модуля, чтобы создать его один раз. На Linux тест проходит, но в окружении с spawn рабочие процессы начинают создавать новые пулы и завершаются ошибкой.
Вариант с глобальным запуском пула прост для чтения, но небезопасен при импорте модуля и усложняет управление жизненным циклом. Вариант с fork может скрыть проблему на одной платформе, однако после создания потоков способен унаследовать некорректное состояние блокировок и не переносится на все окружения.
Выбранное решение — оставить на верхнем уровне только определения конфигурации и функций, а создание пула и запуск event loop разместить в защищённой функции точки входа. Результат: дочерние процессы импортируют модуль без повторного запуска приложения, а асинхронный код сохраняет управление event loop.
Достаточно ли защищать только asyncio.run?
Нет, под защиту нужно помещать весь код, который запускает процессы, создаёт пулы, открывает соединения или выполняет другие побочные эффекты. Определения функций, классов и констант можно оставлять на уровне модуля, если их импорт безопасен.
Создаёт ли spawn копию памяти родительского процесса?
Нет. Дочерний процесс получает новый интерпретатор и только явно переданные сериализуемые данные. Изменение обычной глобальной переменной в дочернем процессе не изменяет такую же переменную в родителе; для обмена нужны очереди, каналы, разделяемая память или другой межпроцессный механизм.
Можно ли передать дочернему процессу корутину вместо обычной функции?
Передавать следует обычную импортируемую функцию, которая внутри запускает или использует asyncio. Объект корутины сам по себе не является выполняемым процессным целевым объектом и обычно не подходит для сериализации. В примере worker — обычная функция, а asyncio.run создаёт event loop уже внутри дочернего процесса.