¿Por qué es asyncio.Future incompatible con concurrent.futures.Future?

Las dos clases representan excelentes abstracciones para la progtwigción concurrente, por lo que es un poco desconcertante que no admitan la misma API.

En concreto, según la documentación :

asyncio.Future es casi compatible con concurrent.futures.Future .

Diferencias

  • result() y exception() no toman un argumento de tiempo de espera y generan una excepción cuando el futuro aún no está terminado.
  • Las devoluciones de add_done_callback() registradas con add_done_callback() siempre se llaman a través del bucle de eventos call_soon_threadsafe() .
  • Esta clase no es compatible con las funciones wait() y as_completed() en el paquete concurrent.futures .

La lista anterior está realmente incompleta, hay algunas diferencias más:

  • running() método de running() está ausente
  • result() y exception() pueden boost InvalidStateError si se llama demasiado pronto

¿Alguno de estos se debe a la naturaleza inherente de un bucle de eventos que hace que estas operaciones sean inútiles o demasiado difíciles de implementar?

¿Y cuál es el significado de la diferencia relacionada con add_done_callback() ? De cualquier manera, se garantiza que la callback sucederá en un momento no especificado después de que se haga el futuro, ¿no es perfectamente coherente entre las dos clases?

La razón principal de la diferencia está en cómo los hilos (y los procesos) manejan los bloques en comparación con cómo las rutinas manejan los eventos que bloquean. En el subprocesamiento, el subproceso actual se suspende hasta que se resuelva cualquier condición y el subproceso pueda avanzar. Por ejemplo, en el caso de los futuros, si solicita el resultado de un futuro, está bien suspender el hilo actual hasta que ese resultado esté disponible.

Sin embargo, el modelo de concurrencia de un bucle de eventos es que, en lugar de suspender el código, regresa al bucle de eventos y se le vuelve a llamar cuando está listo. Por lo tanto, es un error solicitar el resultado de un futuro asyncio que no tiene un resultado listo.

Podría pensar que el futuro de asyncio podría simplemente esperar y si bien eso sería ineficiente, ¿sería realmente tan malo que bloquee su coroutine? Sin embargo, resulta que tener el bloque de coroutine es muy probable que signifique que el futuro nunca se completa. Es muy probable que el resultado del futuro se establezca mediante algún código asociado con el bucle de eventos que ejecuta el código que solicita el resultado. Si el hilo que ejecuta ese bucle de evento se bloquea, no se ejecutará ningún código asociado con el bucle de evento. Por lo tanto, el locking en el resultado podría bloquearse e impedir que se produzca el resultado.

Entonces, sí, las diferencias en la interfaz se deben a esta diferencia inherente. Como ejemplo, no querría usar un futuro asyncio con la abstracción de camarero concurrent.futures porque, de nuevo, eso bloquearía el hilo del bucle de eventos.

La diferencia add_done_callbacks garantiza que las devoluciones de llamada se ejecutarán en el bucle de eventos. Eso es deseable porque obtendrán los datos locales del subproceso del bucle de eventos. Además, una gran cantidad de código común asume que nunca se ejecutará al mismo tiempo que otro código del mismo ciclo de eventos. Es decir, los coroutines solo son seguros para subprocesos bajo el supuesto de que dos coroutines del mismo bucle de eventos no se ejecutan al mismo tiempo. La ejecución de las devoluciones de llamada en el bucle de eventos evita muchos problemas de seguridad de subprocesos y facilita la escritura del código correcto.

concurrent.futures.Future proporciona una forma de compartir resultados entre diferentes subprocesos y procesos, por lo general, cuando usa Executor .

asyncio.Future resuelve la misma tarea, pero para las rutinas , que en realidad son algún tipo especial de funciones que se ejecutan normalmente en un proceso / subproceso de forma asíncrona. “Asincrónicamente” en el contexto actual significa que el bucle de eventos administra el flujo de ejecución de código de estas rutinas: puede suspender la ejecución dentro de una secuencia, comenzar a ejecutar otra y luego volver a ejecutar la primera, todo generalmente en una secuencia / proceso.

Estos objetos (y muchos otros objetos de subprocesos / asínculos como Lock , Event , Semaphore , etc.) son similares porque la idea de concurrencia en su código con subprocesos / procesos y corrutinas es similar.

Creo que la razón principal por la que los objetos son diferentes es histórica: asyncio se creó mucho más tarde, luego threading y concurrent.futures . Probablemente sea imposible cambiar concurrent.futures.Future para trabajar con asyncio sin romper la clase API.

¿Deberían ser ambas clases una en el “mundo ideal”? Este es probablemente un tema discutible, pero veo muchas desventajas de eso: aunque asyncio y los threading parecen similares a primera vista, son muy diferentes en muchos aspectos, incluida la implementación interna o la forma de escribir código asyncio / non-asyncio (vea async / await palabras clave).

Creo que probablemente sea mejor que las clases sean diferentes: claramente nos dividimos de manera diferente por la naturaleza (incluso si su similitud parece extraña al principio).