Exención de responsabilidad: esta es mi primera vez experimentar con el asyncio
módulo.
Estoy usando asyncio.wait
la siguiente manera para tratar de apoyar una función de tiempo de espera para todos los resultados de un conjunto de tareas asíncronas. Esto es parte de una biblioteca más grande, así que estoy omitiendo algo de código irrelevante.
Tenga en cuenta que la biblioteca ya es compatible con la presentación de tareas y el uso de los tiempos de espera con ThreadPoolExecutors y ProcessPoolExecutors, así que no estoy realmente interesado en sugerencias para utilizar aquellos en su lugar o preguntas acerca de por qué estoy haciendo esto con asyncio
. En al código ...
import asyncio
from contextlib import suppress
...
class AsyncIOSubmit(Node):
def get_results(self, futures, timeout=None):
loop = asyncio.get_event_loop()
finished, unfinished = loop.run_until_complete(
asyncio.wait(futures, timeout=timeout)
)
if timeout and unfinished:
# Code options in question would go here...see below.
raise asyncio.TimeoutError
Al principio no estaba preocupado por la cancelación de tareas pendientes en el tiempo de espera, pero luego me dieron el aviso Task was destroyed but it is pending!
de salida del programa o loop.close
. Después de investigar un poco me encontré con varios modos de cancelar tareas y esperar a que en realidad ser cancelado:
Opción 1:
[task.cancel() for task in unfinished]
for task in unfinished:
with suppress(asyncio.CancelledError):
loop.run_until_complete(task)
Opcion 2:
[task.cancel() for task in unfinished]
loop.run_until_complete(asyncio.wait(unfinished))
Opción 3:
# Not really an option for me, since I'm not in an `async` method
# and don't want to make get_results an async method.
[task.cancel() for task in unfinished]
for task in unfinished:
await task
Opción 4:
Una especie de bucle while como en esta respuesta. Parece que mis otras opciones son mejores, pero incluyendo por completo.
Las opciones 1 y 2, ambos parecen muy bien el trabajo hasta el momento. Cualquiera de estas opciones puede ser "correcto", pero con asyncio
la evolución en los últimos años los ejemplos y sugerencias alrededor de la red no están actualizados o bien variar un poco. Así que mis preguntas son ...
Pregunta 1
¿Hay diferencias prácticas entre las opciones 1 y 2? Sé que run_until_complete
se extenderá hasta el futuro se ha completado, por lo que desde la opción 1 es un bucle en un orden específico supongo que podría comportarse de manera diferente si las tareas anteriores tardan más en realidad completa. He intentado mirar el código fuente asyncio de entender si asyncio.wait
sólo lo hace efectivamente lo mismo con sus tareas / futuros bajo el capó, pero no era evidente.
Pregunta 2
Asumo que si una de las tareas es en medio de una operación de bloqueo de larga duración que en realidad no puede cancelar de inmediato? Tal vez eso sólo depende de si la operación subyacente o ser utilizados biblioteca elevará el CancelledError de inmediato o no? Tal vez eso no debería ocurrir nunca con librerías diseñadas para asyncio?
Desde que estoy tratando de implementar una función de tiempo de espera aquí estoy un poco sensible a esto. Si es posible, estas cosas podrían tomar mucho tiempo para cancelar lo consideraría llamar cancel
y no esperar a que ocurra realmente, o la creación de un corto tiempo de espera para esperar a los Cancela a fin.
Pregunta 3
¿Es posible loop.run_until_complete
(o en realidad, la llamada subyacente a async.wait
) devuelve los valores en unfinished
por una razón que no sea un tiempo de espera? Si es así me gustaría, obviamente, tengo que ajustar mi lógica un poco, pero a partir de los documentos que parece que eso no es posible.
¿Hay diferencias prácticas entre las opciones 1 y 2?
Nº Opción 2 se ve mejor y podría ser marginalmente más eficiente, pero su efecto neto es el mismo.
Sé que
run_until_complete
se extenderá hasta el futuro se ha completado, por lo que desde la opción 1 es un bucle en un orden específico supongo que podría comportarse de manera diferente si las tareas anteriores tardan más en realidad completa.
Parece de esa manera al principio, pero en realidad no es el caso porque loop.run_until_complete
ejecuta todas las tareas presentadas al bucle, no sólo el que pasa como argumento. Simplemente se detiene una vez que se complete el awaitable previstos - que es lo que "correr hasta que se completa" se refiere a. Un bucle llamando a run_until_complete
cargo de las tareas ya programadas es como el siguiente código asíncrono:
ts = [asyncio.create_task(asyncio.sleep(i)) for i in range(1, 11)]
# takes 10s, not 55s
for t in ts:
await t
que a su vez semánticamente equivalente al siguiente código de roscado:
ts = []
for i in range(1, 11):
t = threading.Thread(target=time.sleep, args=(i,))
t.start()
ts.append(t)
# takes 10s, not 55s
for t in ts:
t.join()
En otras palabras, await t
y run_until_complete(t)
el bloque hasta que t
se ha completado, pero permitir que todo lo demás - como tareas programadas anteriormente utilizando asyncio.create_task()
para funcionar durante ese tiempo también. Por lo que el tiempo de ejecución total será igual al tiempo de ejecución de la tarea más larga, no de su suma. Por ejemplo, si la primera tarea pasa a llevar mucho tiempo, todos los demás se han terminado en el ínterin, y sus aguarda no dormir en absoluto.
Todo esto sólo se aplica a la espera de las tareas que se han programado previamente. Si intenta aplicar eso a corrutinas, no va a funcionar:
# runs for 55s, as expected
for i in range(1, 11):
await asyncio.sleep(i)
# also 55s
for i in range(1, 11):
t = threading.Thread(target=time.sleep, args=(i,))
t.start()
t.join()
Esto es a menudo un punto de fricción para asyncio principiantes, que escriben código equivalente al último ejemplo asyncio y esperan que se ejecute en paralelo.
He intentado mirar el código fuente asyncio de entender si
asyncio.wait
sólo lo hace efectivamente lo mismo con sus tareas / futuros bajo el capó, pero no era evidente.
asyncio.wait
es sólo una API de conveniencia que hace dos cosas:
- convierte los argumentos de entrada a algo que los implementos
Future
. Para corrutinas que los medios que les plantea al bucle de eventos, como si concreate_task
, lo que les permite ejecutar de forma independiente. Si le dan tareas para empezar, como lo hace, se omite este paso. - usos
add_done_callback
que se le notifique cuando el futuro se hacen, momento en el que vuelve a su persona que llama.
Así que sí, que hace las mismas cosas, pero con una implementación diferente porque es compatible con muchas más características.
Asumo que si una de las tareas es en medio de una operación de bloqueo de larga duración que en realidad no puede cancelar de inmediato?
En asyncio no debería haber "bloquear" las operaciones, sólo aquellos que suspender, y deben ser canceladas inmediatamente. La excepción a esto es el bloqueo de código pegado en asyncio con run_in_executor
, donde la operación subyacente no se cancelará en absoluto, pero la co-rutina asyncio será inmediatamente obtener la excepción.
Tal vez eso sólo depende de si la operación subyacente o ser utilizados biblioteca elevará el CancelledError de inmediato o no?
La biblioteca no recaudar CancelledError
, se recibe en el punto donde ocurrió lo esperan a suspender antes de producirse la cancelación. Para la biblioteca el efecto de la cancelación se await ...
interrumpe su estrategia de esperar e inmediatamente levantar CancelledError
. A no ser atrapado, la excepción se propagará a través de la función y await
llama a todo el camino a la co-rutina de nivel superior, cuya elevar CancelledError
marcas de toda la tarea como cancelada. Buen comportamiento asyncio código hará exactamente eso, posiblemente usando finally
para liberar recursos a nivel de sistema operativo que poseen. Cuando CancelledError
se captura, el código puede optar por no volver a subir, en cuyo caso se ignora la cancelación efectiva.
¿Es posible loop.run_until_complete (o en realidad, la llamada subyacente a
async.wait
) devuelve los valores en inacabada por una razón que no sea un tiempo de espera?
Si está utilizando return_when=asyncio.ALL_COMPLETE
(por defecto), que no debería ser posible. Es bastante posible con return_when=FIRST_COMPLETED
, entonces obviamente es posible independientemente de tiempo de espera.