El multiproceso permite que un proceso ejecute varios subprocesos al mismo tiempo, y los subprocesos comparten la misma memoria y recursos (consulte los diagramas 2 y 4).
Sin embargo, el bloqueo global de intérprete (GIL) de Python limita la efectividad del subproceso múltiple para tareas vinculadas a la CPU.
Bloqueo global de intérprete de Python (GIL)
El GIL es un bloqueo que permite que solo un subproceso mantenga el control del intérprete de Python en cualquier momento, lo que significa que solo un subproceso puede ejecutar el código de bytes de Python a la vez.
GIL se introdujo para simplificar la gestión de la memoria en Python, ya que muchas operaciones internas, como la creación de objetos, no son seguras para subprocesos de forma predeterminada. Sin un GIL, varios subprocesos que intenten acceder a los recursos compartidos requerirán bloqueos complejos o mecanismos de sincronización para evitar condiciones de carrera y corrupción de datos.
¿Cuándo GIL es un cuello de botella?
- Para programas de un solo subproceso, el GIL es irrelevante ya que el subproceso tiene acceso exclusivo al intérprete de Python.
- Para programas multiproceso vinculados a E/S, el GIL es menos problemático ya que los subprocesos liberan el GIL cuando esperan operaciones de E/S.
- Para operaciones multiproceso vinculadas a CPU, GIL se convierte en un cuello de botella importante. Varios subprocesos que compiten por el GIL deben turnarse para ejecutar el código de bytes de Python.
Un caso interesante que vale la pena mencionar es el uso de time.sleepque Python trata efectivamente como una operación de E/S. El time.sleep La función no está vinculada a la CPU porque no implica cálculo activo ni la ejecución de código de bytes de Python durante el período de suspensión. En cambio, la responsabilidad de rastrear el tiempo transcurrido se delega al sistema operativo. Durante este tiempo, el subproceso libera el GIL, lo que permite que otros subprocesos se ejecuten y utilicen el intérprete.
El multiprocesamiento permite que un sistema ejecute múltiples procesos en paralelo, cada uno con su propia memoria, GIL y recursos. Dentro de cada proceso, puede haber uno o más hilos (ver diagramas 3 y 4).
El multiprocesamiento supera las limitaciones del GIL. Esto lo hace adecuado para tareas vinculadas a la CPU que requieren una gran cantidad de cálculo.
Sin embargo, el multiprocesamiento consume más recursos debido a la memoria separada y los gastos generales del proceso.
A diferencia de los subprocesos o procesos, asyncio utiliza un solo subproceso para manejar múltiples tareas.
Al escribir código asincrónico con el asyncio biblioteca, usarás el async/await palabras clave para gestionar tareas.
Conceptos clave
- Corrutinas: Estas son funciones definidas con
async def. Son el núcleo de asyncio y representan tareas que se pueden pausar y reanudar más tarde. - Bucle de eventos: Gestiona la ejecución de tareas.
- Tareas: Envoltorios alrededor de corrutinas. Cuando desea que una rutina comience a ejecutarse, la convierte en una tarea, por ejemplo. usando
asyncio.create_task() await: Pausa la ejecución de una corrutina, devolviendo el control al bucle de eventos.
como funciona
Asyncio ejecuta un bucle de eventos que programa tareas. Las tareas se “pausan” voluntariamente cuando esperan algo, como una respuesta de la red o la lectura de un archivo. Mientras la tarea está en pausa, el bucle de eventos cambia a otra tarea, lo que garantiza que no se pierda tiempo esperando.
Esto hace que asyncio sea ideal para escenarios que involucran muchas tareas pequeñas que pasan mucho tiempo esperandocomo manejar miles de solicitudes web o gestionar consultas de bases de datos. Dado que todo se ejecuta en un solo subproceso, asyncio evita la sobrecarga y la complejidad del cambio de subprocesos.
La diferencia clave entre asyncio y multithreading radica en cómo manejan las tareas en espera.
- El subproceso múltiple depende del sistema operativo para cambiar entre subprocesos cuando un subproceso está en espera (cambio de contexto preventivo).
Cuando un subproceso está en espera, el sistema operativo cambia automáticamente a otro subproceso. - Asyncio usa un solo hilo y depende de las tareas para “cooperar” haciendo una pausa cuando necesitan esperar (multitarea cooperativa).
2 formas de escribir código asíncrono:
method 1: await coroutine
cuando tu directamente await una corrutina, la ejecución de la pausas de rutina actuales al await declaración hasta que finalice la rutina esperada. Las tareas se ejecutan. secuencialmente dentro de la corrutina actual.
Utilice este enfoque cuando necesite el resultado de la corrutina. inmediatamente para continuar con los siguientes pasos.
Aunque esto pueda parecer código síncrono, no lo es. En código síncrono, todo el programa se bloquearía durante una pausa.
Con asyncio, solo la rutina actual se detiene, mientras que el resto del programa puede continuar ejecutándose. Esto hace que asyncio no bloquee a nivel de programa.
Ejemplo:
El bucle de eventos pausa la rutina actual hasta que fetch_data está completo.
async def fetch_data():
print("Fetching data...")
await asyncio.sleep(1) # Simulate a network call
print("Data fetched")
return "data"async def main():
result = await fetch_data() # Current coroutine pauses here
print(f"Result: {result}")
asyncio.run(main())
method 2: asyncio.create_task(coroutine)
La corrutina está programada para ejecutar simultáneamente en segundo plano. A diferencia de awaitla rutina actual continúa ejecutándose inmediatamente sin esperar a que finalice la tarea programada.
La rutina programada comienza a ejecutarse tan pronto como el bucle de eventos encuentra una oportunidad.sin necesidad de esperar una respuesta explícita await.
No se crean nuevos hilos; en cambio, la corrutina se ejecuta dentro del mismo hilo que el bucle de eventos, que gestiona cuándo cada tarea obtiene tiempo de ejecución.
Este enfoque permite la concurrencia dentro del programa, lo que permite que múltiples tareas se superpongan en su ejecución de manera eficiente. Más tarde necesitarás await la tarea para obtener su resultado y asegurarse de que se realice.
Utilice este enfoque cuando desee ejecutar tareas simultáneamente y no necesite los resultados de inmediato.
Ejemplo:
cuando la línea asyncio.create_task() se alcanza, la corrutina fetch_data() está programado para comenzar a funcionar inmediatamente cuando el bucle de eventos esté disponible. Esto puede suceder incluso antes usted explícitamente await la tarea. En cambio, en la primera await método, la corrutina sólo comienza a ejecutarse cuando el await se llega a la declaración.
En general, esto hace que el programa sea más eficiente al superponer la ejecución de múltiples tareas.
async def fetch_data():
# Simulate a network call
await asyncio.sleep(1)
return "data"async def main():
# Schedule fetch_data
task = asyncio.create_task(fetch_data())
# Simulate doing other work
await asyncio.sleep(5)
# Now, await task to get the result
result = await task
print(result)
asyncio.run(main())
Otros puntos importantes
- Puede mezclar código sincrónico y asincrónico.
Dado que el código sincrónico está bloqueando, se puede descargar a un hilo separado usandoasyncio.to_thread(). Esto hace que su programa sea efectivamente multiproceso.
En el siguiente ejemplo, el bucle de eventos asyncio se ejecuta en el hilo principal, mientras que se usa un hilo en segundo plano separado para ejecutar elsync_task.
import asyncio
import timedef sync_task():
time.sleep(2)
return "Completed"
async def main():
result = await asyncio.to_thread(sync_task)
print(result)
asyncio.run(main())
- Debe descargar las tareas vinculadas a la CPU que requieren un uso intensivo de cómputo a un proceso separado.
Este flujo es una buena manera de decidir cuándo usar qué.
- Multiprocesamiento
– Lo mejor para tareas vinculadas a la CPU que requieren un uso intensivo de computación.
– Cuando necesitas omitir el GIL: cada proceso tiene su propio intérprete de Python, lo que permite un verdadero paralelismo. - subprocesos múltiples
– Lo mejor para tareas rápidas vinculadas a E/S, ya que la frecuencia de cambio de contexto se reduce y el intérprete de Python se apega a un solo subproceso durante más tiempo.
– No es ideal para tareas vinculadas a la CPU debido a GIL. - asincio
– Ideal para tareas lentas vinculadas a E/S, como solicitudes de red largas o consultas de bases de datos, porque maneja eficientemente la espera, lo que la hace escalable.
– No apto para tareas vinculadas a la CPU sin descargar trabajo a otros procesos.
Eso es todo amigos. Hay mucho más que cubrir este tema, pero espero haberle presentado los diversos conceptos y cuándo usar cada método.
¡Gracias por leer! Escribo regularmente sobre Python, el desarrollo de software y los proyectos que construyo, así que sígueme para no perdértelo. Nos vemos en el próximo artículo 🙂