Concurrencia en Python

    Introducci贸n

    La inform谩tica ha evolucionado con el tiempo y han surgido m谩s y m谩s formas de hacer que las computadoras funcionen a煤n m谩s r谩pido. 驴Qu茅 pasa si en lugar de ejecutar una sola instrucci贸n a la vez, tambi茅n podemos ejecutar varias instrucciones al mismo tiempo? Esto significar铆a un aumento significativo en el rendimiento de un sistema.

    A trav茅s de la concurrencia, podemos lograr esto y nuestros programas de Python podr谩n manejar a煤n m谩s solicitudes a la vez y, con el tiempo, lo que generar谩 impresionantes ganancias de rendimiento.

    En este art铆culo, discutiremos la concurrencia en el contexto de la programaci贸n Python, las diversas formas en las que se presenta y aceleraremos un programa simple para ver las ganancias de rendimiento en la pr谩ctica.

    驴Qu茅 es la concurrencia?

    Cuando dos o m谩s eventos son concurrentes, significa que est谩n sucediendo al mismo tiempo. En la vida real, la concurrencia es com煤n, ya que suceden muchas cosas al mismo tiempo todo el tiempo. En inform谩tica, las cosas son un poco diferentes cuando se trata de concurrencia.

    En inform谩tica, la concurrencia es la ejecuci贸n de trabajos o tareas por una computadora al mismo tiempo. Normalmente, una computadora ejecuta un trabajo mientras otros esperan su turno, una vez que se completa, los recursos se liberan y el siguiente trabajo comienza a ejecutarse. Este no es el caso cuando se implementa la simultaneidad, ya que las piezas de trabajo a ejecutar no siempre tienen que esperar a que se completen otras. Se ejecutan al mismo tiempo.

    Simultaneidad vs paralelismo

    Hemos definido la concurrencia como la ejecuci贸n de tareas al mismo tiempo, pero 驴c贸mo se compara con el paralelismo y qu茅 es?

    El paralelismo se logra cuando se realizan m煤ltiples c谩lculos u operaciones al mismo tiempo o en paralelo con el objetivo de acelerar el proceso de c谩lculo.

    Tanto la concurrencia como el paralelismo est谩n involucrados en la realizaci贸n de m煤ltiples tareas simult谩neamente, pero lo que las distingue es el hecho de que, si bien la concurrencia solo tiene lugar en un procesador, el paralelismo se logra mediante la utilizaci贸n de m煤ltiples CPU para realizar tareas en paralelo.

    Hilo vs proceso vs tarea

    Aunque en t茅rminos generales, los hilos, procesos y tareas pueden referirse a piezas o unidades de trabajo. Sin embargo, en detalle no son tan similares.

    Un hilo es la unidad de ejecuci贸n m谩s peque帽a que se puede realizar en una computadora. Los subprocesos existen como partes de un proceso y, por lo general, no son independientes entre s铆, lo que significa que comparten datos y memoria con otros subprocesos dentro del mismo proceso. Los subprocesos tambi茅n se denominan a veces procesos ligeros.

    Por ejemplo, en una aplicaci贸n de procesamiento de documentos, un subproceso podr铆a ser responsable de formatear el texto y otro maneja el guardado autom谩tico, mientras que otro est谩 haciendo correcciones ortogr谩ficas.

    Un proceso es un trabajo o una instancia de un programa calculado que se puede ejecutar. Cuando escribimos y ejecutamos c贸digo, se crea un proceso para ejecutar todas las tareas que le hemos indicado a la computadora que haga a trav茅s de nuestro c贸digo. Un proceso puede tener un solo subproceso primario o tener varios subprocesos dentro de 茅l, cada uno con su propia pila, registros y contador de programa. Pero todos comparten el c贸digo, los datos y la memoria.

    Algunas de las diferencias comunes entre procesos e hilos son:

    • Los procesos funcionan de forma aislada, mientras que los subprocesos pueden acceder a los datos de otros subprocesos
    • Si un subproceso dentro de un proceso est谩 bloqueado, otros subprocesos pueden continuar ejecut谩ndose, mientras que un proceso bloqueado pondr谩 en espera la ejecuci贸n de los otros procesos en la cola.
    • Mientras que los subprocesos comparten memoria con otros subprocesos, los procesos no y cada proceso tiene su propia asignaci贸n de memoria.

    Una tarea es simplemente un conjunto de instrucciones de programa que se cargan en la memoria.

    Multithreading vs multiprocesamiento vs Asyncio

    Habiendo explorado los hilos y procesos, profundicemos ahora en las diversas formas en que una computadora se ejecuta al mismo tiempo.

    El subproceso m煤ltiple se refiere a la capacidad de una CPU para ejecutar varios subprocesos al mismo tiempo. La idea aqu铆 es dividir un proceso en varios subprocesos que se pueden ejecutar de forma paralela o al mismo tiempo. Esta divisi贸n de funciones mejora la velocidad de ejecuci贸n de todo el proceso. Por ejemplo, en un procesador de texto como MS Word, suceden muchas cosas cuando est谩 en uso.

    El subproceso m煤ltiple permitir谩 al programa guardar autom谩ticamente el contenido que se est谩 escribiendo, realizar correcciones ortogr谩ficas del contenido y tambi茅n formatear el contenido. A trav茅s del multiproceso, todo esto puede tener lugar simult谩neamente y el usuario no tiene que completar el documento primero para que se guarde o se realicen las revisiones ortogr谩ficas.

    Solo un procesador est谩 involucrado durante el subproceso m煤ltiple y el sistema operativo decide cu谩ndo cambiar las tareas en el procesador actual, estas tareas pueden ser externas al proceso o programa actual que se est谩 ejecutando en nuestro procesador.

    El multiprocesamiento, por otro lado, implica la utilizaci贸n de dos o m谩s unidades de procesador en una computadora para lograr el paralelismo. Python implementa el multiprocesamiento creando diferentes procesos para diferentes programas, y cada uno tiene su propia instancia del int茅rprete de Python para ejecutar y la asignaci贸n de memoria para utilizar durante la ejecuci贸n.

    AsyncIO o IO asincr贸nico es un nuevo paradigma introducido en Python 3 con el prop贸sito de escribir c贸digo concurrente usando la sintaxis async / await. Es mejor para prop贸sitos de redes de alto nivel y de E / S.

    Cu谩ndo usar la concurrencia

    Las ventajas de la simultaneidad se aprovechan mejor al resolver problemas vinculados a CPU o IO.

    Los problemas relacionados con la CPU involucran programas que realizan muchos c谩lculos sin requerir instalaciones de red o almacenamiento y solo est谩n limitados por las capacidades de la CPU.

    Los problemas vinculados a E / S involucran programas que dependen de recursos de entrada / salida que a veces pueden ser m谩s lentos que la CPU y generalmente est谩n en uso, por lo tanto, el programa tiene que esperar a que la tarea actual libere los recursos de E / S.

    Es mejor escribir c贸digo concurrente cuando la CPU o los recursos de E / S son limitados y desea acelerar su programa.

    C贸mo utilizar la concurrencia

    En nuestro ejemplo de demostraci贸n, resolveremos un problema com煤n de enlace de E / S, que es la descarga de archivos a trav茅s de una red. Escribiremos c贸digo no concurrente y c贸digo concurrente y compararemos el tiempo que tarda cada programa en completarse.

    Descargaremos im谩genes de Imgur a trav茅s de su API. Primero, necesitamos crear una cuenta y luego Registrarse nuestra aplicaci贸n de demostraci贸n para acceder a la API y descargar algunas im谩genes.

    Una vez que nuestra aplicaci贸n est茅 configurada en Imgur, recibiremos un identificador de cliente y un secreto de cliente que usaremos para acceder a la API. Guardaremos las credenciales en un .env archivo desde Pipenv carga autom谩ticamente las variables del .env archivo.

    Script sincr贸nico

    Con esos detalles, podemos crear nuestro primer script que simplemente descargar谩 un mont贸n de im谩genes a un downloads carpeta:

    import os
    from urllib import request
    from imgurpython import ImgurClient
    import timeit
    
    client_secret = os.getenv("CLIENT_SECRET")
    client_id = os.getenv("CLIENT_ID")
    
    client = ImgurClient(client_id, client_secret)
    
    def download_image(link):
        filename = link.split("https://Pharos.sh.com/")[3].split('.')[0]
        fileformat = link.split("https://Pharos.sh.com/")[3].split('.')[1]
        request.urlretrieve(link, "downloads/{}.{}".format(filename, fileformat))
        print("{}.{} downloaded into downloads/ folder".format(filename, fileformat))
    
    def main():
        images = client.get_album_images('PdA9Amq')
        for image in images:
            download_image(image.link)
    
    if __name__ == "__main__":
        print("Time taken to download images synchronously: {}".format(timeit.Timer(main).timeit(number=1)))
    

    En este script, pasamos un identificador de 谩lbum de Imgur y luego descargamos todas las im谩genes en ese 谩lbum usando la funci贸n get_album_images(). Esto nos da una lista de las im谩genes y luego usamos nuestra funci贸n para descargar las im谩genes y guardarlas en una carpeta localmente.

    Este sencillo ejemplo hace el trabajo. Podemos descargar im谩genes de Imgur pero no funciona al mismo tiempo. Solo descarga una imagen a la vez antes de pasar a la siguiente. En mi m谩quina, el script tard贸 48 segundos en descargar las im谩genes.

    Optimizaci贸n con subprocesos m煤ltiples

    Hagamos ahora nuestro c贸digo concurrente usando Multithreading y veamos c贸mo funciona:

    # previous imports from synchronous version are maintained
    import threading
    from concurrent.futures import ThreadPoolExecutor
    
    # Imgur client setup remains the same as in the synchronous version
    
    # download_image() function remains the same as in the synchronous
    
    def download_album(album_id):
        images = client.get_album_images(album_id)
        with ThreadPoolExecutor(max_workers=5) as executor:
            executor.map(download_image, images)
    
    def main():
        download_album('PdA9Amq')
    
    if __name__ == "__main__":
        print("Time taken to download images using multithreading: {}".format(timeit.Timer(main).timeit(number=1)))
    

    En el ejemplo anterior, creamos un Threadpool y configure 5 hilos diferentes para descargar im谩genes de nuestra galer铆a. Recuerde que los subprocesos se ejecutan en un solo procesador.

    Esta versi贸n de nuestro c贸digo tarda 19 segundos. Eso es casi tres veces m谩s r谩pido que la versi贸n sincr贸nica del script.

    Optimizaci贸n con multiprocesamiento

    Ahora implementaremos multiprocesamiento en varias CPU para el mismo script para ver c贸mo funciona:

    # previous imports from synchronous version remain
    import multiprocessing
    
    # Imgur client setup remains the same as in the synchronous version
    
    # download_image() function remains the same as in the synchronous
    
    def main():
        images = client.get_album_images('PdA9Amq')
    
        pool = multiprocessing.Pool(multiprocessing.cpu_count())
        result = pool.map(download_image, [image.link for image in images])
    
    if __name__ == "__main__":
        print("Time taken to download images using multiprocessing: {}".format(timeit.Timer(main).timeit(number=1)))
    

    En esta versi贸n, creamos un grupo que contiene la cantidad de n煤cleos de CPU en nuestra m谩quina y luego asignamos nuestra funci贸n para descargar las im谩genes en el grupo. Esto hace que nuestro c贸digo se ejecute de manera paralela en nuestra CPU y esta versi贸n de multiprocesamiento de nuestro c贸digo tarda un promedio de 14 segundos despu茅s de m煤ltiples ejecuciones.

    Esto es un poco m谩s r谩pido que nuestra versi贸n que utiliza subprocesos y significativamente m谩s r谩pido que nuestra versi贸n no concurrente.

    Optimizaci贸n con AsyncIO

    Implementemos el mismo script usando AsyncIO para ver c贸mo funciona:

    # previous imports from synchronous version remain
    import asyncio
    import aiohttp
    
    # Imgur client setup remains the same as in the synchronous version
    
    async def download_image(link, session):
        """
        Function to download an image from a link provided.
        """
        filename = link.split("https://Pharos.sh.com/")[3].split('.')[0]
        fileformat = link.split("https://Pharos.sh.com/")[3].split('.')[1]
    
        async with session.get(link) as response:
            with open("downloads/{}.{}".format(filename, fileformat), 'wb') as fd:
                async for data in response.content.iter_chunked(1024):
                    fd.write(data)
    
        print("{}.{} downloaded into downloads/ folder".format(filename, fileformat))
    
    async def main():
        images = client.get_album_images('PdA9Amq')
    
        async with aiohttp.ClientSession() as session:
            tasks = [download_image(image.link, session) for image in images]
    
            return await asyncio.gather(*tasks)
    
    if __name__ == "__main__":
        start_time = timeit.default_timer()
    
        loop = asyncio.get_event_loop()
        results = loop.run_until_complete(main())
    
        time_taken = timeit.default_timer() - start_time
    
        print("Time taken to download images using AsyncIO: {}".format(time_taken))
    

    Hay algunos cambios que se destacan en nuestro nuevo gui贸n. Primero, ya no usamos lo normal requests m贸dulo para descargar nuestras im谩genes, pero en su lugar usamos aiohttp. La raz贸n de esto es que requests es incompatible con AsyncIO ya que usa Python http y sockets m贸dulo.

    Los sockets se bloquean por naturaleza, es decir, no se pueden pausar y la ejecuci贸n continuar m谩s adelante. aiohttp resuelve esto y nos ayuda a lograr un c贸digo verdaderamente asincr贸nico.

    La palabra clave async indica que nuestra funci贸n es una corrutina (rutina cooperativa), que es un fragmento de c贸digo que se puede pausar y reanudar. Las corrutinas realizan m煤ltiples tareas de forma cooperativa, lo que significa que eligen cu谩ndo hacer una pausa y dejar que otros la ejecuten.

    Creamos un pool donde hacemos una cola de todos los enlaces a las im谩genes que deseamos descargar. Nuestra corrutina se inicia coloc谩ndola en el bucle de eventos y ejecut谩ndola hasta su finalizaci贸n.

    Despu茅s de varias ejecuciones de este script, la versi贸n de AsyncIO tarda 14 segundos en promedio en descargar las im谩genes del 谩lbum. Esto es significativamente m谩s r谩pido que las versiones multiproceso y s铆ncronas del c贸digo, y bastante similar a la versi贸n multiprocesamiento.

    Comparaci贸n de rendimiento

    Asyncio multiproceso multiproceso s铆ncrono

    48 s19 a帽os14 s14 s

    Conclusi贸n

    En esta publicaci贸n, hemos cubierto la concurrencia y c贸mo se compara con el paralelismo. Tambi茅n hemos explorado los diversos m茅todos que podemos usar para implementar la simultaneidad en nuestro c贸digo Python, incluidos el multiproceso y el multiprocesamiento, y tambi茅n discutimos sus diferencias.

    En los ejemplos anteriores, podemos ver c贸mo la simultaneidad ayuda a que nuestro c贸digo se ejecute m谩s r谩pido de lo que lo har铆a de manera s铆ncrona. Como regla general, el multiprocesamiento es m谩s adecuado para tareas vinculadas a la CPU, mientras que el subproceso m煤ltiple es mejor para tareas vinculadas a E / S.

    El c贸digo fuente de esta publicaci贸n est谩 disponible en GitHub para referencia.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *