Introducci贸n
Contenido
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 s | 19 a帽os | 14 s | 14 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.