Python 3.5 Implementando concurrencia usando asyncio

Traducción del Capítulo 13 Concurrencia
del libro 'Expert Python Programming',
Segunda edicion
Michał Jaworski y Tarek Ziadé, 2016

Programación asincrónica


En los últimos años, la programación asincrónica ha ganado gran popularidad. Python 3.5 finalmente obtuvo algunas funciones de sintaxis que refuerzan los conceptos de soluciones asincrónicas. Pero esto no significa que la programación asincrónica haya sido posible solo desde Python 3.5. Muchas bibliotecas y marcos se proporcionaron mucho antes, y la mayoría de ellos se originaron en versiones anteriores de Python 2. Incluso hay una implementación alternativa completa de Python llamada Stackless (consulte el Capítulo 1, "El estado actual de Python"), que se centra en este enfoque de programación único. Para algunas soluciones, como Twisted, Tornado o Eventlet , las comunidades activas todavía existen y realmente vale la pena conocerlas. En cualquier caso, comenzando con Python 3.5, la programación asincrónica se ha vuelto más fácil que nunca. Por lo tanto, se espera que sus funciones asincrónicas integradas reemplacen la mayoría de las herramientas antiguas, o los proyectos externos se convertirán gradualmente en una especie de marcos de alto nivel basados ​​en Python incorporado.

Al tratar de explicar qué es la programación asincrónica, es más fácil pensar en este enfoque como algo similar a los subprocesos, pero sin un programador del sistema. Esto significa que un programa asíncrono puede procesar tareas al mismo tiempo, pero su contexto se cambia internamente y no por el planificador del sistema.

Pero, por supuesto, no usamos hilos para el procesamiento paralelo de tareas en un programa asincrónico. La mayoría de las soluciones usan conceptos diferentes y, según la implementación, se llaman de manera diferente. Algunos ejemplos de nombres utilizados para describir dichos objetos de programas paralelos son:

  • Hilos verdes: hilos verdes (proyectos greenlet, gevent o eventlet)
  • Coroutines - coroutines (programación puramente asincrónica en Python 3.5)
  • Tasklets (Python apilable) Estos son básicamente los mismos conceptos, pero a menudo se implementan de maneras ligeramente diferentes.

Por razones obvias, en esta sección nos centraremos solo en las rutinas que inicialmente son compatibles con Python, comenzando con la versión 3.5.

Multitarea colaborativa y E / S asíncrona


La multitarea colaborativa es el núcleo de la programación asincrónica. En este sentido, la multitarea en el sistema operativo no es necesaria para iniciar un cambio de contexto (a otro proceso o subproceso), sino que cada proceso libera voluntariamente el control cuando está en modo de espera para garantizar la ejecución simultánea de varios programas. Por eso se llama colaborativo. Todos los procesos deben trabajar juntos para garantizar que la multitarea sea exitosa.

El modelo multitarea a veces se usaba en sistemas operativos, pero ahora difícilmente se puede encontrar como una solución a nivel de sistema. Esto se debe a que existe el riesgo de que un servicio mal diseñado pueda alterar fácilmente la estabilidad de todo el sistema. La programación de subprocesos y procesos utilizando cambios de contexto controlados directamente por el sistema operativo es actualmente el enfoque dominante para la concurrencia a nivel del sistema. Pero la multitarea colaborativa sigue siendo una gran herramienta de concurrencia a nivel de aplicación.

Hablando de la multitarea conjunta a nivel de aplicación, no estamos tratando con hilos o procesos que necesitan liberar control, ya que toda la ejecución está contenida en un proceso y un hilo. En cambio, tenemos varias tareas (corutinas, tasklets e hilos verdes) que transfieren el control a una única función que controla la coordinación de las tareas. Esta función suele ser una especie de bucle de eventos.

Para evitar confusiones (debido a la terminología de Python), ahora llamaremos a estas tareas paralelas corutinas. La cuestión más importante en la multitarea colaborativa es cuándo transferir el control. En la mayoría de las aplicaciones asíncronas, el control se pasa al planificador o al bucle de eventos durante las operaciones de E / S. Independientemente de si el programa lee datos del sistema de archivos o se comunica a través de un socket, dicha operación de E / S siempre está asociada con algún tiempo de espera cuando el proceso se vuelve inactivo. La latencia depende de un recurso externo, por lo que esta es una buena oportunidad para liberar el control para que otras corutinas puedan hacer su trabajo, hasta que también tengan que esperar a que este enfoque tenga un comportamiento similar al de la implementación de subprocesos múltiples en Python. Sabemos que GIL serializa subprocesos de Python, pero también se libera con cada operación de E / S. La principal diferencia es que los subprocesos en Python se implementan como subprocesos a nivel de sistema, por lo que el sistema operativo puede descargar el subproceso actualmente en ejecución en cualquier momento y transferir el control a otro.

En la programación asincrónica, las tareas nunca son interrumpidas por el bucle de eventos principal. Es por eso que este estilo multitarea también se llama multitarea no prioritaria.

Por supuesto, cada aplicación Python se ejecuta en un sistema operativo donde hay otros procesos que compiten por los recursos. Esto significa que el sistema operativo siempre tiene el derecho de descargar todo el proceso y transferir el control a otro. Pero cuando nuestra aplicación asincrónica comienza de nuevo, continúa desde donde se detuvo cuando intervino el programador del sistema. Es por eso que las corutinas en este contexto se consideran no apiñadas.

Python asíncrono y espera palabras clave


Las palabras clave async y wait son los bloques de construcción principales en la programación asincrónica de Python.

La palabra clave asíncrona utilizada antes de la declaración def define una nueva rutina. Una función de rutina puede suspenderse y reanudarse bajo circunstancias estrictamente definidas. Su sintaxis y comportamiento son muy similares a los generadores (consulte el Capítulo 2, "Recomendaciones de sintaxis", debajo del nivel de clase). De hecho, los generadores deberían usarse en versiones anteriores de Python para implementar corutinas. Aquí hay un ejemplo de declaración de una función que usa la palabra clave asíncrona :

async def async_hello(): print("hello, world!") 

Las funciones definidas con la palabra clave asíncrona son especiales. Cuando se les llama, no ejecutan código dentro, sino que devuelven un objeto de rutina:

 >>>> async def async_hello(): ... print("hello, world!") ... >>> async_hello() <coroutine object async_hello at 0x1014129e8> 

El objeto de rutina no hace nada hasta que su ejecución esté programada en el bucle de eventos. El módulo asyncio está disponible para proporcionar una implementación básica del bucle de eventos, así como muchas otras utilidades asincrónicas:

 >>> import asyncio >>> async def async_hello(): ... print("hello, world!") ... >>> loop = asyncio.get_event_loop() >>> loop.run_until_complete(async_hello()) hello, world! >>> loop.close() 

Naturalmente, al crear solo una simple rutina, en nuestro programa no implementamos paralelismo. Para ver algo realmente paralelo, necesitamos crear más tareas que serán realizadas por un ciclo de eventos.

Se pueden agregar nuevas tareas al bucle llamando al método loop.create_task () o proporcionando otro objeto para esperar a que se use la función asyncio.wait () . Usaremos el último enfoque e intentaremos imprimir asincrónicamente una secuencia de números generados usando la función range () :

 import asyncio async def print_number(number): print(number) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete( asyncio.wait([ print_number(number) for number in range(10) ]) ) loop.close() 

La función asyncio.wait () acepta una lista de objetos de rutina y regresa de inmediato. El resultado es un generador que produce objetos que representan resultados futuros (futuros). Como su nombre lo indica, se usa para esperar a que se completen todas las rutinas proporcionadas. La razón por la que devuelve un generador en lugar de un objeto de rutina es porque es compatible con versiones anteriores de Python, lo que se explicará más adelante. El resultado de ejecutar este script puede ser el siguiente:

 $ python asyncprint.py 0 7 8 3 9 4 1 5 2 6 

Como podemos ver, los números no se imprimen en el orden en que creamos nuestras corutinas. Pero esto es exactamente lo que queríamos lograr.

La segunda palabra clave importante agregada en Python 3.5 está a la espera . Se utiliza para esperar los resultados de un evento futuro o de rutina (explicado más adelante) y liberar el control sobre la ejecución en el bucle de eventos. Para comprender mejor cómo funciona esto, debemos considerar un ejemplo de código más complejo.

Supongamos que queremos crear dos corutinas que realicen algunas tareas simples en un bucle:

  • Espera un número aleatorio de segundos
  • Imprima un texto proporcionado como argumento y la cantidad de tiempo que pasó esperando. Comencemos con una implementación simple que tiene algunos problemas de concurrencia que intentaremos mejorar más adelante con el uso adicional de wait:

     import time import random import asyncio async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 time.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) async def main(): await asyncio.wait([waiter("foo"), waiter("bar")]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() 

Cuando se ejecuta en la terminal (usando el comando de tiempo para medir el tiempo), puede ver:

 $ time python corowait.py bar waited 0.25 seconds bar waited 0.25 seconds bar waited 0.5 seconds bar waited 0.5 seconds foo waited 0.75 seconds foo waited 0.75 seconds foo waited 0.25 seconds foo waited 0.25 seconds real 0m3.734s user 0m0.153s sys 0m0.028s 


Como podemos ver, ambas corutinas completaron su ejecución, pero no de forma asincrónica. La razón es que ambos usan la función time.sleep () , que bloquea pero no libera el control en el bucle de eventos. Esto funcionará mejor en una instalación de subprocesos múltiples, pero no queremos usar transmisiones en este momento. Entonces, ¿cómo podemos solucionar esto?

La respuesta es usar asyncio.sleep () , que es una versión asincrónica de time.sleep (), y esperar el resultado usando la palabra clave wait. Ya usamos esta declaración en la primera versión de main () , pero esto fue solo para mejorar la claridad del código. Esto claramente no hizo que nuestra implementación fuera más paralela. Veamos una versión mejorada de la rutina de waiter () que usa wait asyncio.sleep ():

 async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 await asyncio.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) 


Al ejecutar el script actualizado, veremos cómo la salida de dos funciones se alternan entre sí:

 $ time python corowait_improved.py bar waited 0.25 seconds foo waited 0.25 seconds bar waited 0.25 seconds foo waited 0.5 seconds foo waited 0.25 seconds bar waited 0.75 seconds foo waited 0.25 seconds bar waited 0.5 seconds real 0m1.953s user 0m0.149s sys 0m0.026s 


Un beneficio adicional de esta simple mejora es que el código se ejecuta más rápido. El tiempo total de ejecución fue menor que la suma de todos los tiempos de sueño, porque las corutinas tomaron el control una por una.

Asyncio en versiones anteriores de Python


El módulo asyncio apareció en Python 3.4. Entonces, esta es la única versión de Python que tiene soporte serio para la programación asincrónica anterior a Python 3.5. Desafortunadamente, parece que estas dos versiones posteriores son suficientes para presentar problemas de compatibilidad.

De todos modos, el núcleo de programación asincrónico en Python se introdujo antes que los elementos de sintaxis que admiten esta plantilla. Más vale tarde que nunca, pero esto creó una situación en la que hay dos sintaxis para trabajar con las rutinas.

Comenzando con Python 3.5, puede usar async y esperar :

 async def main (): await asyncio.sleep(0) 


Sin embargo, en Python 3.4, tendrá que aplicar adicionalmente el decorador asyncio.coroutine y ceder en el texto de la rutina:

 @asyncio.couroutine def main(): yield from asyncio.sleep(0) 


Otro hecho útil es que el rendimiento de la declaración se introdujo en Python 3.3, y PyPI tiene un puerto asíncrono. Esto significa que también puede usar esta implementación de multitarea colaborativa con Python 3.3.

Un ejemplo práctico de programación asincrónica.


Como se mencionó muchas veces en este capítulo, la programación asincrónica es una gran herramienta para manejar E / S. Es hora de crear algo más práctico que simplemente imprimir secuencias o esperar asincrónicamente.

Para garantizar la coherencia, intentaremos resolver el mismo problema que hemos resuelto con la ayuda de multiprocesamiento y multiprocesamiento. Por lo tanto, intentaremos extraer asincrónicamente algunos datos de recursos externos a través de una conexión de red. Sería genial si pudiéramos usar el mismo paquete python-gmaps que en las secciones anteriores. Lamentablemente, no podemos.

El creador de python-gmaps era un poco vago y solo tomó el nombre. Para simplificar el desarrollo, eligió el paquete de solicitud como su biblioteca de cliente HTTP. Desafortunadamente, las solicitudes no admiten E / S asíncronas con asíncrono y esperan . Hay otros proyectos que tienen como objetivo proporcionar cierto paralelismo para el proyecto de consulta, pero confían en Gevent ( grequests , consulte https://github.com/ kennethreitz / grequests ) o ejecutan un grupo de subprocesos / procesos (query-futuros) consulte github.com/ross/requests-futures ). Ninguno de ellos resuelve nuestro problema.

Antes de reprocharme por regañar a un inocente desarrollador de código abierto, cálmate. La persona detrás del paquete python-gmaps soy yo. Una mala elección de dependencias es uno de los problemas de este proyecto. Solo me gusta criticarme públicamente de vez en cuando. Esta será una amarga lección para mí, ya que python-gmaps en su última versión (0.3.1 al momento de escribir este libro) no puede integrarse fácilmente con la E / S asíncrona de Python. En cualquier caso, esto puede cambiar en el futuro, por lo que no se pierde nada.
Conociendo las limitaciones de la biblioteca, que era tan fácil de usar en los ejemplos anteriores, necesitamos crear algo que llene este vacío. Google MapsAPI es realmente fácil de usar, por lo que crearemos una utilidad asincrónica solo a modo de ilustración. La biblioteca estándar de Python 3.5 aún carece de una biblioteca que pueda ejecutar solicitudes HTTP asincrónicas tan fácilmente como llamar a urllib.urlopen () . Definitivamente no queremos crear un soporte de protocolo completo desde cero, por lo que utilizaremos un poco de ayuda del paquete aiohttp disponible en PyPI. Esta es una biblioteca realmente prometedora que agrega implementaciones tanto de cliente como de servidor para HTTP asíncrono. Aquí hay un pequeño módulo construido sobre aiohttp que crea una función auxiliar geocode () que ejecuta solicitudes de geocodificación al servicio API de Google Maps:

 import aiohttp session = aiohttp.ClientSession() async def geocode(place): params = { 'sensor': 'false', 'address': place } async with session.get( 'https://maps.googleapis.com/maps/api/geocode/json', params=params ) as response: result = await response.json() return result['results'] 


Supongamos que este código se almacena en un módulo llamado asyncgmaps , que usaremos más adelante. Ahora estamos listos para reescribir el ejemplo utilizado en la discusión de subprocesamiento múltiple y multiprocesamiento. Anteriormente, solíamos separar toda la operación en dos etapas separadas:

  1. Cumplir todas las solicitudes al servicio externo en paralelo utilizando la función fetch_place () .
  2. Muestra todos los resultados en un bucle con la función present_result () .

Pero dado que la multitarea colaborativa es completamente diferente al uso de múltiples procesos o hilos, podemos cambiar ligeramente nuestro enfoque. La mayoría de los problemas planteados en el uso de un solo hilo por elemento ya no son nuestra preocupación.
Las corutinas no son preventivas, por lo que podemos mostrar fácilmente los resultados inmediatamente después de recibir respuestas HTTP. Esto simplificará nuestro código y lo hará más comprensible:

 import asyncio # note: local module introduced earlier from asyncgmaps import geocode, session PLACES = ( 'Reykjavik', 'Vien', 'Zadar', 'Venice', 'Wrocław', 'Bolognia', 'Berlin', 'Słubice', 'New York', 'Dehli', ) async def fetch_place(place): return (await geocode(place))[0] async def present_result(result): geocoded = await result print("{:>25s}, {:6.2f}, {:6.2f}".format( geocoded['formatted_address'], geocoded['geometry']['location']['lat'], geocoded['geometry']['location']['lng'], )) async def main(): await asyncio.wait([ present_result(fetch_place(place)) for place in PLACES ]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) # aiohttp will raise issue about unclosed # ClientSession so we perform cleanup manually loop.run_until_complete(session.close()) loop.close() 


La programación asincrónica es excelente para los desarrolladores de backend interesados ​​en crear aplicaciones escalables. En la práctica, esta es una de las herramientas más importantes para crear servidores altamente competitivos.

Pero la realidad es triste. Muchos paquetes populares que se ocupan de problemas de E / S no están destinados a ser utilizados con código asíncrono. Las principales razones para esto son:

  • Todavía baja implementación de Python 3 y algunas de sus características avanzadas
  • Baja comprensión de varios conceptos de concurrencia entre principiantes para aprender Python

Esto significa que muy a menudo la migración de aplicaciones y paquetes síncronos multiproceso existentes es imposible (debido a restricciones arquitectónicas) o demasiado costosa. Muchos proyectos podrían beneficiarse enormemente de la implementación del estilo multitarea asíncrono, pero solo unos pocos eventualmente lo harán. Esto significa que ahora tendrá muchas dificultades para crear aplicaciones asincrónicas desde el principio. En la mayoría de los casos, esto será similar al problema mencionado en la sección "Ejemplo práctico de programación asincrónica": interfaces incompatibles y bloqueo no sincrónico de las operaciones de E / S. Por supuesto, a veces puedes dejar de esperar cuando experimentas tal incompatibilidad y solo obtienes los recursos necesarios sincrónicamente. Pero esto impedirá que la otra rutina ejecute su código mientras espera los resultados. Técnicamente, esto funciona, pero también destruye todos los beneficios de la programación asincrónica. Por lo tanto, al final, combinar E / S asíncrona con E / S sincrónica no es una opción. Este es un juego de todo o nada.

Otro problema son las largas operaciones vinculadas al procesador. Cuando realiza una operación de E / S, no hay problema para liberar el control de una rutina. Al escribir / leer desde un sistema de archivos o socket, eventualmente esperará, por lo que una llamada usando waitit es lo mejor que puede hacer. Pero, ¿qué pasa si necesita calcular algo y sabe que tomará algún tiempo? Por supuesto, puede dividir el problema en partes y cancelar el control cada vez que avance un poco el trabajo. Pero pronto descubrirá que este no es un muy buen modelo. Tal cosa puede hacer que el código sea desordenado, y tampoco garantiza buenos resultados.

El enlace temporal debe ser responsabilidad del intérprete o del sistema operativo.

Combinando código asincrónico con futuros asincrónicos


Entonces, ¿qué hacer si tiene un código que realiza E / S sincrónicas largas que no puede o no desea reescribir? ¿O qué hacer cuando tiene que realizar algunas operaciones de procesador pesadas en una aplicación diseñada principalmente para E / S asincrónicas? Bueno ... necesitas encontrar una solución. Y con eso quiero decir multiproceso o multiprocesamiento.

Puede que esto no suene muy bien, pero a veces la mejor solución puede ser lo que intentamos escapar. El procesamiento paralelo de tareas intensivas en recursos en Python siempre se realiza mejor debido al multiprocesamiento. Y el subprocesamiento múltiple puede manejar las operaciones de E / S igualmente bien (rápidamente y sin muchos recursos), como asíncrono y en espera si se configura y maneja con cuidado.

Entonces, a veces, cuando no sabes qué hacer cuando algo simplemente no se ajusta a tu aplicación asincrónica, usa un código que lo coloca en un hilo o proceso separado. Puede fingir que es una rutina, liberar el control del bucle de eventos y, en última instancia, procesar los resultados cuando estén listos.

Afortunadamente para nosotros, la biblioteca estándar de Python proporciona el módulo concurrent.futures , que también está integrado con el módulo asyncio . Juntos, estos dos módulos le permiten planificar las funciones de bloqueo que se ejecutan en subprocesos o procesos adicionales, como si fueran corridas asincrónicas sin bloqueo.

Ejecutores y futuros


Antes de ver cómo incrustar subprocesos o procesos en un bucle de eventos asíncrono, veamos más de cerca el módulo concurrent.futures , que luego se convertirá en el componente principal de nuestra llamada solución alternativa.

Las clases más importantes en el módulo concurrent.futures son Executor y Future .

Executor es un conjunto de recursos que puede procesar elementos de trabajo en paralelo. Puede parecer muy similar en propósito a las clases del módulo multiprocesador - Pool y dummy.Pool - pero tiene una interfaz y una semántica completamente diferentes. Esta es una clase base que no está destinada a implementarse y tiene dos implementaciones específicas:

  • ThreadPoolExecutor : que representa un grupo de subprocesos
  • ProcessPoolExecutor : que representa un grupo de procesos

Cada ejecutor presenta tres métodos:

  • submit (fn, * args, ** kwargs) : programa la función fn para que se ejecute en el grupo de recursos y devuelve un objeto Future que representa la ejecución del objeto llamado
  • map (func, * iterables, timeout = None, chunksize = 1) : la función func se ejecuta en la iteración de manera similar al multiprocesamiento. Método Pool.map ()
  • shutdown (wait = True) : esto apaga al ejecutor y libera todos sus recursos.

El método más interesante es submit () debido al objeto Future que devuelve. Representa la ejecución asincrónica de la llamada y solo representa indirectamente su resultado. Para obtener el valor de retorno real del objeto llamado despachado, debe llamar al método Future.result () . Y si el objeto llamado ya está completado, el método result () no lo bloqueará y simplemente devolverá la salida de la función. Si no es así, lo bloqueará hasta que el resultado esté listo. Piense en ello como una promesa de un resultado (en realidad es el mismo concepto que una promesa en JavaScript). No necesita desempacarlo inmediatamente después de recibirlo (usando el método result () ), pero si intenta hacer esto, se garantiza que eventualmente devolverá algo:

 >>> def loudy_return(): ... print("processing") ... return 42 ... >>> from concurrent.futures import ThreadPoolExecutor >>> with ThreadPoolExecutor(1) as executor: ... future = executor.submit(loudy_return) ... processing >>> future <Future at 0x33cbf98 state=finished returned int> >>> future.result() 42 


Si desea utilizar el método Executor.map () , su uso no difiere del método Pool.map () de la clase Pool del módulo multiprocesador:

 def main(): with ThreadPoolExecutor(POOL_SIZE) as pool: results = pool.map(fetch_place, PLACES) for result in results: present_result(result) 


Usar ejecutor en un bucle de eventos


Las instancias de la clase Future devueltas por el método Executor.submit () están conceptualmente muy cerca de las corutinas utilizadas en la programación asincrónica. Es por eso que podemos usar artistas para crear un híbrido entre la multitarea colaborativa y el multiprocesamiento o subprocesamiento múltiple.

El núcleo de esta solución es el método BaseEventLoop.run_in_executor (ejecutor, func, * args) de la clase de bucle de eventos . Esto le permite planificar la ejecución de la función func en un proceso o grupo de subprocesos representado por el argumento del ejecutor. Lo más importante de este método es que devuelve el nuevo objeto esperado (el objeto que se puede esperar utilizando el operador de espera). Por lo tanto, gracias a esto, puede realizar una función de bloqueo que no es una corutina exactamente como una corutina, y no se bloqueará, sin importar cuánto tiempo tarde en terminar. Solo detendrá la función que espera resultados de dicha llamada, pero el ciclo completo de eventos continuará.

Y un hecho útil es que ni siquiera necesita crear su propia instancia de ejecutor. Si pasa Ninguno como argumento al ejecutor , la clase ThreadPoolExecutor se usará con el número predeterminado de subprocesos (para Python 3.5, este es el número de procesadores multiplicado por 5).

Entonces, supongamos que no queríamos reescribir la parte problemática del paquete python-gmaps que estaba causando nuestro dolor de cabeza. Podemos diferir fácilmente una llamada de bloqueo a un hilo separado llamando a loop.run_in_executor () , mientras dejamos la función fetch_place () como la rutina esperada:

 async def fetch_place(place): coro = loop.run_in_executor(None, api.geocode, place) result = await coro return result[0] 


Tal solución es peor que tener una biblioteca completamente asíncrona para hacer el trabajo, pero sabe que al menos algo es mejor que nada.

Después de explicar qué es realmente la concurrencia, tomamos medidas y analizamos uno de los problemas paralelos típicos usando el subprocesamiento múltiple. Después de identificar las principales deficiencias de nuestro código y corregirlas, recurrimos al multiprocesamiento para ver cómo funcionaría en nuestro caso.

Después de eso, descubrimos que con un módulo multiprocesador, usar varios procesos es mucho más fácil que los subprocesos básicos con subprocesos múltiples. Pero solo después de eso nos dimos cuenta de que podemos usar la misma API con subprocesos, gracias a multiprocessing.dummy. Por lo tanto, la elección entre multiprocesamiento y subprocesamiento múltiple ahora depende solo de qué solución se adapta mejor al problema y no qué solución tiene la mejor interfaz.

Hablando de adaptar el problema, finalmente probamos la programación asincrónica, que debería ser la mejor solución para las aplicaciones relacionadas con E / S, solo para comprender que no podemos olvidarnos completamente de los hilos y procesos. ¡Así que hicimos un círculo, de vuelta a donde empezamos!

Y esto nos lleva a la conclusión final de este capítulo. No hay una solución que se adapte a todos. Hay varios enfoques que puede preferir o preferir más. Hay algunos enfoques que se adaptan mejor a este conjunto de problemas, pero debe conocerlos todos para tener éxito. En escenarios realistas, puede usar todo el arsenal de herramientas y estilos de paralelismo en una sola aplicación, y esto no es raro.

La conclusión anterior es una excelente introducción al tema del próximo capítulo, Capítulo 14 "Patrones de diseño útiles". Dado que no hay una plantilla única que resuelva todos sus problemas. Debes saber tanto como sea posible, porque finalmente los usarás todos los días.

Source: https://habr.com/ru/post/484446/


All Articles