Разберите ситуацию: синхронная функция уже запустила event loop, а затем пытается вызвать asyncio.run для корутины. Каков результат?
Вызов asyncio.run внутри уже работающего event loop завершается исключением RuntimeError. Эта функция предназначена для верхнеуровневого запуска корутины и сама создаёт, запускает и закрывает event loop, поэтому вложенный запуск в том же потоке запрещён.
Вместо этого вызывающий код должен передать корутину уже работающему циклу: использовать await, asyncio.create_task или другой подход, соответствующий архитектуре приложения.
Асинхронные приложения раньше нередко вручную создавали event loop, запускали его, обрабатывали завершение задач и закрывали ресурсы. Такой код был многословным и часто приводил к ошибкам при финализации асинхронных генераторов, задач и пула потоков.
asyncio.run появился как единая точка входа для запуска асинхронной программы из синхронного кода. Он рассчитан на внешний, верхнеуровневый вызов, а не на управление циклом изнутри другой корутины.
Работающий event loop уже управляет очередью задач, обработкой событий и выполнением текущей корутины. Попытка запустить внутри него второй цикл в том же потоке нарушила бы модель владения циклом и сделала бы неочевидным, какой цикл должен обрабатывать события и завершать задачи.
Неверное решение обычно встречается в обработчиках веб-фреймворков, Jupyter и других системах, где цикл уже запущен. Последствие — немедленный RuntimeError, а созданная непосредственно перед вызовом корутина может дополнительно вызвать предупреждение о том, что она не была обработана.
Перед запуском asyncio.run Python проверяет, существует ли уже работающий event loop в текущем потоке. Если существует, вложенный запуск запрещается. При обычном внешнем вызове asyncio.run создаёт новый цикл, выполняет переданную корутину, ожидает завершения асинхронных генераторов и закрывает цикл вместе с используемыми им ресурсами.
Внутри асинхронного кода нужно не запускать новый цикл, а продолжить работу в текущем:
Здесь asyncio.run вызывается только на внешней границе программы. Внутри handler корутина вызывается через await, а независимая работа — через create_task.
Если синхронный код находится в другом потоке и ему нужно передать корутину уже работающему циклу, применяют механизм вроде asyncio.run_coroutine_threadsafe. Это отдельный сценарий межпоточного взаимодействия; использовать его для обхода ошибки в том же потоке нельзя, поскольку ожидание результата может заблокировать сам event loop.
Создание нового цикла в отдельном потоке иногда допустимо, но это уже отдельная асинхронная среда. Объекты, привязанные к конкретному циклу или потоку, например некоторые транспортные объекты и примитивы синхронизации, нельзя бездумно переносить между такими средами.
Веб-приложение вызывает синхронный метод библиотеки, а тот внутри пытается получить результат асинхронного клиента через asyncio.run. В рабочем процессе обработчик уже выполняется внутри event loop, поэтому запросы начинают завершаться исключением.
Рассматривались три варианта. Вызов нового цикла в отдельном потоке сохраняет синхронный интерфейс, но добавляет накладные расходы и усложняет передачу контекста. Создание нового event loop в том же потоке не решает проблему: работающий цикл там уже существует.
Выбранным решением стала переделка границы библиотеки в асинхронный метод, который принимает и ожидает корутину через await. Для независимых запросов использовали create_task с последующим явным ожиданием; это сохранило один event loop, исключило вложенный запуск и упростило контроль отмены и ошибок.
Нет. create_task должен вызываться внутри работающего event loop и возвращает задачу, которую нужно сохранить или дождаться. Он не блокирует синхронный вызывающий код и не является универсальной заменой для запуска асинхронной программы из обычной функции.
Если внешняя функция синхронная и event loop ещё не запущен, корректной верхнеуровневой точкой обычно остаётся asyncio.run. Если цикл уже запущен, синхронный интерфейс нужно либо преобразовать в асинхронный, либо явно организовать взаимодействие через другой поток.
Вызов асинхронной функции создаёт объект-корутину, но её тело начинает выполняться только после передачи в await, задачу или другой механизм исполнения. Поэтому простая передача корутины в синхронную функцию не заменяет запуск event loop.
Это также объясняет предупреждение о необработанной корутине: объект был создан, но ни разу не был передан исполнителю. В частности, при выражении вроде asyncio.run(fetch()) объект корутины создаётся до входа в asyncio.run, поэтому ошибка о уже работающем цикле может сопровождаться таким предупреждением.
Проверка выполняется относительно текущего потока. Если в другом потоке нет работающего event loop, asyncio.run может создать и запустить собственный цикл там.
Однако это не делает безопасным произвольный обмен асинхронными объектами между потоками. Циклы, транспортные объекты и некоторые примитивы могут быть привязаны к своему потоку или циклу. Для передачи корутины в уже работающий цикл из другого потока следует использовать предназначенный для этого межпоточный механизм и аккуратно обрабатывать результат, исключения и отмену.