¿Cuándo usar y cuándo no usar Python 3.5 `await`?
Estoy entendiendo el flujo de uso asyncio
en Python 3.5 pero no he visto una descripción de qué cosas debería await
hacer y qué no debería hacer o dónde sería insignificante. ¿Tengo que usar mi mejor criterio en términos de "esta es una operación IO y, por lo tanto, debería await
editarse"?
De forma predeterminada, todo su código es sincrónico. Puede hacerlo asíncrono definiendo funciones async def
y "llamando" a estas funciones con await
. Una pregunta más correcta sería "¿Cuándo debo escribir código asincrónico en lugar de sincrónico?". La respuesta es "Cuando puedas beneficiarte de ello". En los casos en que trabaje con operaciones de E/S como señaló, generalmente se beneficiará:
# Synchronous way:
download(url1) # takes 5 sec.
download(url2) # takes 5 sec.
# Total time: 10 sec.
# Asynchronous way:
await asyncio.gather(
async_download(url1), # takes 5 sec.
async_download(url2) # takes 5 sec.
)
# Total time: only 5 sec. (+ little overhead for using asyncio)
Por supuesto, si creó una función que usa código asincrónico, esta función también debería ser asincrónica (debe definirse como async def
). Pero cualquier función asincrónica puede utilizar libremente código síncrono. No tiene sentido convertir código síncrono a asíncrono sin algún motivo:
# extract_links(url) should be async because it uses async func async_download() inside
async def extract_links(url):
# async_download() was created async to get benefit of I/O
html = await async_download(url)
# parse() doesn't work with I/O, there's no sense to make it async
links = parse(html)
return links
Una cosa muy importante es que cualquier operación sincrónica larga (> 50 ms, por ejemplo, es difícil decirlo exactamente) congelará todas sus operaciones asincrónicas durante ese tiempo:
async def extract_links(url):
data = await download(url)
links = parse(data)
# if search_in_very_big_file() takes much time to process,
# all your running async funcs (somewhere else in code) will be frozen
# you need to avoid this situation
links_found = search_in_very_big_file(links)
Puede evitar que llame a funciones síncronas de larga ejecución en procesos separados (y espere el resultado):
executor = ProcessPoolExecutor(2)
async def extract_links(url):
data = await download(url)
links = parse(data)
# Now your main process can handle another async functions while separate process running
links_found = await loop.run_in_executor(executor, search_in_very_big_file, links)
Un ejemplo más: cuando necesitas usarlo requests
en asyncio. requests.get
es solo una función síncrona de larga duración, a la que no debes llamar dentro del código asíncrono (nuevamente, para evitar la congelación). Pero tarda mucho debido a la E/S, no a causa de cálculos prolongados. En ese caso, puedes usar ThreadPoolExecutor
en lugar de ProcessPoolExecutor
para evitar algunos gastos generales de multiprocesamiento:
executor = ThreadPoolExecutor(2)
async def download(url):
response = await loop.run_in_executor(executor, requests.get, url)
return response.text
No tienes mucha libertad. Si necesita llamar a una función, debe averiguar si se trata de una función habitual o de una corrutina. Debe utilizar la await
palabra clave si y sólo si la función que está llamando es una corrutina.
Si async
hay funciones involucradas, debería haber un "bucle de eventos" que orqueste estas async
funciones. Estrictamente hablando, no es necesario, puede ejecutar "manualmente" el async
método enviándole valores, pero probablemente no quiera hacerlo. El bucle de eventos realiza un seguimiento de las corrutinas que aún no han terminado y elige la siguiente para continuar ejecutándose. asyncio
El módulo proporciona una implementación de bucle de eventos, pero esta no es la única implementación posible.
Considere estas dos líneas de código:
x = get_x()
do_something_else()
y
x = await aget_x()
do_something_else()
La semántica es absolutamente la misma: llama a un método que produce algún valor, cuando el valor esté listo, asígnalo a la variable x
y haz otra cosa. En ambos casos, la do_something_else
función se llamará solo después de que finalice la línea de código anterior. Ni siquiera significa que antes, después o durante la ejecución del aget_x
método asincrónico el control se cederá al bucle de eventos.
Aún así hay algunas diferencias:
- el segundo fragmento solo puede aparecer dentro de otra
async
función aget_x
la función no es habitual, sino una rutina (que se declara conasync
una palabra clave o se adorna como una rutina)aget_x
es capaz de "comunicarse" con el bucle de eventos: es decir, cederle algunos objetos. El bucle de eventos debería poder interpretar estos objetos como solicitudes para realizar algunas operaciones (por ejemplo, enviar una solicitud de red y esperar una respuesta, o simplemente suspender esta rutina porn
unos segundos). La función habitualget_x
no puede comunicarse con el bucle de eventos.