“asyncio.run() no se puede llamar desde un bucle de eventos en ejecución” cuando se usa Jupyter Notebook

6 minutos de lectura

Avatar de usuario de Chan
Chan

Me gustaría usar asyncio para obtener la página web html.

Ejecuto el siguiente código en jupyter notebook:

import aiofiles
import aiohttp
from aiohttp import ClientSession

async def get_info(url, session):
    resp = await session.request(method="GET", url=url)
    resp.raise_for_status()
    html = await resp.text(encoding='GB18030')
    with open('test_asyncio.html', 'w', encoding='utf-8-sig') as f:
        f.write(html)
    return html
    
async def main(urls):
    async with ClientSession() as session:
        tasks = [get_info(url, session) for url in urls]
        return await asyncio.gather(*tasks)

if __name__ == "__main__":
    url = ['http://huanyuntianxiazh.fang.com/house/1010123799/housedetail.htm', 'http://zhaoshangyonghefu010.fang.com/house/1010126863/housedetail.htm']
    result = asyncio.run(main(url))

Sin embargo, vuelve RuntimeError: asyncio.run() cannot be called from a running event loop

¿Cuál es el problema?

¿Cómo resolverlo?

avatar de usuario de cglacet
cglacet

los asyncio.run() documentación dice:

Esta función no poder se llamará cuando se esté ejecutando otro bucle de eventos asyncio en el mismo subproceso.

En tu caso, jupyter (Python ≥ 7.0) ya está ejecutando un bucle de eventos:

Ahora puede usar async/await en el nivel superior en la terminal de IPython y en la computadora portátil; en la mayoría de los casos, debería, en la mayoría de los casos, “simplemente funcionar”. Actualice IPython a la versión 7+, IPykernel a la versión 5+, y estará listo para la carrera.

Por lo tanto, no necesita iniciar el ciclo de eventos usted mismo y, en su lugar, puede llamar await main(url) directamente, incluso si su código se encuentra fuera de cualquier función asíncrona.

Jupyter/IPython

async def main():
    print(1)
    
await main()

Python (≥ 3.7) o versiones anteriores de IPython

import asyncio

async def main():
    print(1)
    
asyncio.run(main())

En tu código eso daría:

url = ['url1', 'url2']
result = await main(url)

for text in result:
    pass # text contains your html (text) response

Precaución

Hay un Pequeña diferencia sobre cómo Jupyter usa el bucle en comparación con IPython.

  • Gracias, cglacet. Sin embargo, hay una advertencia: c:\program files\python37\lib\site-packages\ipykernel_launcher.py:29: RuntimeWarning: coroutine 'main' was never awaited

    – Chan

    29 de marzo de 2019 a las 2:32

  • Eso es probablemente porque llamaste main(url) en vez de await main(url).

    – cglacet

    29 de marzo de 2019 a las 2:33

  • ¿Hay alguna manera de tener un fragmento de código que funcione tanto dentro como fuera de Jupyter?

    – Foad S. Farimani

    2 de abril de 2020 a las 10:58

  • No sé si es posible, pero no estoy seguro de que realmente quieras eso de todos modos. Tal vez haga esto como una pregunta separada y vea si las personas tienen ideas sobre si es posible o deseable.

    – cglacet

    11 de abril de 2020 a las 13:06

  • Curiosamente, cuando ejecuto lo anterior en jupyter, obtengo: SyntaxError: ‘await’ outside function

    – Lucas

    27 de julio de 2020 a las 22:55


Avatar de usuario de Jean Monet
Juan Monet

Para añadir a cglacetLa respuesta de – si uno quiere detectar si un bucle se está ejecutando y ajustar automáticamente (es decir, ejecutar main() en el bucle existente, de lo contrario asyncio.run()), aquí hay un fragmento que puede resultar útil:

# async def main():
#     ...

try:
    loop = asyncio.get_running_loop()
except RuntimeError:  # 'RuntimeError: There is no current event loop...'
    loop = None

if loop and loop.is_running():
    print('Async event loop already running. Adding coroutine to the event loop.')
    tsk = loop.create_task(main())
    # ^-- https://docs.python.org/3/library/asyncio-task.html#task-object
    # Optionally, a callback function can be executed when the coroutine completes
    tsk.add_done_callback(
        lambda t: print(f'Task done with result={t.result()}  << return val of main()'))
else:
    print('Starting new event loop')
    result = asyncio.run(main())

  • Bien, de hecho, agregar una devolución de llamada aquí es probablemente la única forma de recuperar el resultado. Probablemente sería interesante tener este código en una función, por lo que solo tendría que llamar a algo como run(main(*args, **kwargs)). La función también podría asegurarse de recuperar su salida, aunque no estoy seguro de que sea realmente simple (debido a la devolución de llamada).

    – cglacet

    29 abr 2020 a las 22:35


  • Esta es una solución intrigante. ¿Cómo obtendrías t.result() en una variable en lugar de imprimirla?

    – alec_djinn

    10 de agosto a las 12:13

  • @alec_djinn no hay una respuesta fácil inmediata (al menos no de dentro del código de sincronización), desde la introducción de un método de bloqueo en el hilo actual, es decir. en código de sincronización (como Queue.get()con un Queue.put(t.result()) en la devolución de llamada) también bloquearía la ejecución del bucle asíncrono existente (en el hilo actual), por lo que no llegaría ningún resultado -> interbloqueo. Si está en un Jupyter NB, aún puede usar un sistema de mensajería y verificar manualmente el resultado (sin bloqueo). Nota usando time.sleep() en el código de sincronización (subproceso actual) no ayuda, ya que también pausaría el bucle asíncrono existente.

    –Jean Monet

    10 de agosto a las 13:08


Solo usa esto:

https://github.com/erdewit/nest_asyncio

import nest_asyncio
nest_asyncio.apply()

  • los documentos para nest_asyncio mencionar un informe de problemas con asyncio donde se indica explícitamente que este no es un comportamiento previsto para asyncio. Entonces consideraría nest_asyncio un gran truco en el que no confiaría en mi base de código para no romper todo con una nueva versión de Python.

    – Gregor Mullegger

    14 oct 2020 a las 11:29


  • Otro problema es que nest_asyncio requiere Python 3.5 o superior, lo que no me resulta útil porque estoy atascado en Python 2.7.

    – mknote

    17 de noviembre de 2020 a las 21:23

  • esto me permitió resolver el problema del código importado (donde no puedes cambiarlo tú mismo)

    – Álex S.

    30 de noviembre de 2020 a las 17:16

  • Esto funcionó muy bien solucionando un problema de asyncio importado, muchas gracias

    – Mika C.

    9 de mayo a las 19:15

Avatar de usuario de Mark
Marca

Combinando los métodos de Pankaj Sharma y Jean Monet, escribí el siguiente fragmento que actúa como asyncio.run (con una sintaxis ligeramente diferente), pero también funciona dentro de un cuaderno Jupyter.

class RunThread(threading.Thread):
    def __init__(self, func, args, kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.result = None
        super().__init__()

    def run(self):
        self.result = asyncio.run(self.func(*self.args, **self.kwargs))

def run_async(func, *args, **kwargs):
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None
    if loop and loop.is_running():
        thread = RunThread(func, args, kwargs)
        thread.start()
        thread.join()
        return thread.result
    else:
        return asyncio.run(func(*args, **kwargs))

Uso:

async def test(name):
    await asyncio.sleep(5)
    return f"hello {name}"

run_async(test, "user")  # blocks for 5 seconds and returns "hello user"

Encontré el unsync paquete útil para escribir código que se comporte de la misma manera en una secuencia de comandos de Python y en Jupyter REPL.

import asyncio
from unsync import unsync


@unsync
async def demo_async_fn():
    await asyncio.sleep(0.1)
    return "done!"

print(demo_async_fn().result())

Avatar de usuario de Pankaj Sharma
Pankaj Sharma

Como cglacet mencionó que la documentación dice

No se puede llamar a esta función cuando se está ejecutando otro bucle de eventos asyncio en el mismo subproceso.

Puedes usar otro hilo, es decir:

class ResolveThread(threading.Thread):
            def __init__(self,result1,fun,url):
                self.result1= result1
                self.fun = fun
                self.url = url
                threading.Thread.__init__(self)
            def run(self):
                result1[0] = asyncio.run(self.fun(self.url))


result1 = [None]
sp = ResolveThread(result1)
sp.start()
sp.join() # connect main thread
result = result1[0]

¿Ha sido útil esta solución?