El multiproceso permite que un proceso ejecute varios subprocesos simultáneamente, 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érpretes de Python (GIL)
El GIL es un bloqueo que permite que un solo subproceso controle el 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.
El GIL se introdujo para simplificar la gestión de la memoria en Python, porque muchas operaciones internas, como la creación de objetos, no son seguras para subprocesos de forma predeterminada. Sin GIL, múltiples subprocesos que intenten acceder a 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 porque el subproceso tiene acceso exclusivo al intérprete de Python.
- Para programas multiproceso vinculados a E/S, el GIL es menos problemático porque los subprocesos liberan el GIL mientras esperan operaciones de E/S.
- Para operaciones multiproceso vinculadas a la CPU, el 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 a destacar es el uso de time.sleep
que Python trata efectivamente como una operación de E/S. EL time.sleep
La función no está vinculada a la CPU, ya que no implica cálculo activo ni 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 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 evita las limitaciones de GIL. Esto lo hace adecuado para tareas vinculadas a la CPU que requieren cálculos pesados.
Sin embargo, el multiprocesamiento requiere más recursos debido a la memoria separada y a 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, utilizará el async/await
palabras clave para gestionar tareas.
Conceptos clave
- Corrutinas: Estas son funciones definidas con
async def
. Están en el corazón de asyncio y representan tareas que pueden suspenderse y reanudarse 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
: suspende la ejecución de una corrutina y devuelve el control al bucle de eventos.
como funciona
Asyncio ejecuta un bucle de eventos que programa tareas. Las tareas se «pausan» voluntariamente cuando están esperando algo, como una respuesta de la red o un archivo para leer. 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 requieren mucho tiempo de esperacomo manejar miles de solicitudes web o manejar 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 principal diferencia entre asyncio y multithreading es cómo manejan las tareas pendientes.
- El subproceso múltiple depende del sistema operativo para cambiar entre subprocesos cuando un subproceso está en espera (cambio de contexto preventivo).
Cuando un hilo está en espera, el sistema operativo cambia automáticamente a otro hilo. - Asyncio usa un solo hilo y depende de las tareas para «cooperar» deteniéndose cuando necesitan esperar (multitarea cooperativa).
2 formas de escribir código asincrónico:
method 1: await coroutine
cuando tu directamente await
una corrutina, ejecutando el pausa la rutina actual hacia await
declaración hasta que se complete la rutina esperada. Las tareas se ejecutan. secuencialmente en la rutina actual.
Utilice este enfoque cuando necesite el resultado de la rutina. inmediatamente para proceder a 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 tiene como objetivo ejecutar simultáneamente en segundo plano. Al contrario de await
la rutina actual continúa ejecutándose inmediatamente sin esperar a que se complete 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 en el 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 eficientemente en su ejecución. tendrás que hacerlo más tarde await
la tarea de lograr su resultado y garantizar que se haga.
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á previsto entrar en funcionamiento inmediatamente cuando el bucle de eventos esté disponible. Esto incluso puede pasar Antes usted explícitamente await
la tarea. Por otro lado, en la primera await
método, la corrutina sólo comienza a ejecutarse cuando el await
se logra 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 se bloquea, 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 que requieren un uso intensivo de la CPU y de cálculo a un proceso separado.
Este flujo es una buena manera de decidir cuándo usar qué.
- Multiprocesamiento
– Ideal para tareas vinculadas a la CPU que requieren muchos cálculos.
– Cuando necesitas omitir el GIL: cada proceso tiene su propio intérprete de Python, lo que permite un verdadero paralelismo. - subprocesos múltiples
– Ideal para tareas rápidas vinculadas a E/S porque 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 relacionadas con E/S, como solicitudes de red largas o consultas de bases de datos, ya que maneja eficazmente 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. Este tema necesita cubrir mucho más, pero espero haberle presentado los diferentes conceptos y cuándo usar cada método.
¡Gracias por leer! Escribo regularmente sobre Python, el desarrollo de software y los proyectos que creo, así que sígueme para no perdértelo. Nos vemos en el próximo artículo 🙂