Estado Compartido y Límites de Aislamiento
Python ofrece hilos, procesos, concurrent.futures, subintérpretes y ahora una compilación con hilos libres, lo que puede parecer una caja de herramientas inusualmente grande para un solo problema.
En realidad, es un pequeño número de herramientas situadas en un espectro: cuánta memoria se permite compartir entre dos piezas de código concurrente y qué mecanismo evita que esa memoria compartida se corrompa.
Cada herramienta de esta sección es una respuesta específica a esa misma pregunta, no una característica no relacionada.
Fundamentos de Concurrencia introduce la distinción entre concurrencia y paralelismo, y Elección de un Modelo de Concurrencia ofrece una lista de verificación práctica para la toma de decisiones; threading, multiprocessing, El GIL y Python con Hilos Libres y Subintérpretes cubren en profundidad un punto específico del espectro.
Esta página es la capa subyacente a todas ellas: el único eje en el que todas tienen posiciones diferentes y por qué ese eje es lo que realmente decide qué herramienta se adapta a una carga de trabajo dada.
Resumen
- Cada mecanismo de concurrencia de Python se intercambia a lo largo de un eje: cuánta memoria se comparte entre unidades de trabajo concurrentes y qué impone seguridad sobre esa memoria compartida.
- Por Qué Importa: Elegir el punto incorrecto en ese espectro deja el paralelismo real sobre la mesa (usando hilos para trabajo limitado por CPU) o paga un costo de aislamiento innecesario (usando procesos para trabajo limitado por E/S que los hilos manejan bien).
- Conceptos Clave: memoria compartida, límite de aislamiento, el GIL, proceso, estado del intérprete, hilos libres.
- Cuándo Usar: Elegir entre hilos, procesos, subintérpretes o async para una carga de trabajo dada, y comprender por qué la misma tarea ("ejecutar esto concurrentemente") necesita herramientas diferentes dependiendo de si está limitada por CPU o por E/S.
- Limitaciones / Intercambios: Más aislamiento proporciona más paralelismo real pero cuesta más sobrecarga: un proceso es más pesado que un hilo, y la comunicación entre límites (serialización, sockets, IPC) nunca es tan barata como pasar un objeto simple por referencia.
- Temas Relacionados: el Global Interpreter Lock,
multiprocessing, ejecutores deconcurrent.futures, subintérpretes, compilaciones de Python con hilos libres.
Fundamentos
Dos o más piezas de código ejecutándose "al mismo tiempo" siempre tienen que responder a una pregunta subyacente: ¿qué sucede si ambas intentan leer o modificar la misma pieza de memoria a la vez?
El modelo de concurrencia de un lenguaje es, en esencia, un conjunto de respuestas a esa pregunta, y Python ofrece varias porque diferentes cargas de trabajo quieren respuestas diferentes.
En un extremo se encuentra compartición completa con un único bloqueo: un hilo se ejecuta dentro del mismo proceso que todos los demás hilos, ve exactamente los mismos objetos en memoria y, en la compilación CPython predeterminada, solo un hilo ejecuta bytecode de Python en cualquier instante porque el Global Interpreter Lock (GIL) impone esa regla.
En el otro extremo se encuentra ninguna compartición en absoluto: un proceso (a través de multiprocessing) tiene su propio espacio de memoria privado, su propio intérprete y su propio GIL, por lo que dos procesos pueden ejecutar bytecode de Python genuinamente simultáneamente en núcleos separados; el precio es que no ven automáticamente los objetos del otro, y cualquier cosa que se pase entre ellos tiene que ser serializada (pickled) a través del límite.
Una analogía útil: los hilos son compañeros de trabajo en una oficina compartida, capaces de acceder al mismo archivador instantáneamente pero solo uno a la vez por regla de la casa; los procesos son oficinas separadas en edificios separados, cada una con su propio archivador, capaces de trabajar simultáneamente pero necesitando un mensajero (serialización) para intercambiar cualquier papeleo.
# hilos: misma memoria, mismos objetos, un GIL - seguro por serialización, no por aislamiento
counter = {"n": 0}
# cada hilo ve este dict exacto; se necesita un bloqueo para incrementarlo de forma segura
# procesos: memoria separada, objetos separados - seguro por aislamiento, no por bloqueo
# un proceso hijo recibe su propia copia; los cambios no aparecen automáticamente en el padreMecánicas e Interacciones
El GIL es el mecanismo que hace seguro el modelo de memoria totalmente compartida predeterminado de CPython: garantiza que solo un hilo ejecuta bytecode de Python a la vez, lo que significa que la mayoría de las operaciones únicas en objetos de Python no se interrumpen ni se corrompen incluso sin un bloqueo explícito, a costa de que los hilos nunca ejecuten código Python verdaderamente en paralelo entre sí.
Ese único hecho explica la división a la que esta sección sigue volviendo: los hilos ayudan al trabajo limitado por E/S, porque las operaciones de E/S liberan el GIL mientras esperan en la red o en el disco, permitiendo que otro hilo se ejecute durante esa espera; pero los hilos no ayudan al trabajo limitado por CPU, porque un bucle de Python ajustado nunca libera el GIL, por lo que N hilos realizando computación pura no se ejecutan más rápido que uno.
multiprocessing evita el GIL por completo al dar a cada trabajador su propio intérprete y su propio GIL, que es por lo que es la respuesta predeterminada para el paralelismo limitado por CPU en CPython ordinario; el aislamiento que hace necesaria la serialización es exactamente el mismo aislamiento que hace posible la ejecución paralela real.
concurrent.futures no introduce un nuevo punto en este espectro; es una interfaz Executor uniforme (submit, map) sobre dos existentes: ThreadPoolExecutor para el lado de memoria compartida, ProcessPoolExecutor para el lado aislado; por lo que cambiar entre ellos es un cambio de una línea una vez que el código está escrito contra la API Executor en lugar de contra hilos o procesos directamente.
Memoria compartida, un GIL Memoria aislada, un GIL cada uno
------------------------- ------------------------------
threading.Thread multiprocessing.Process
ThreadPoolExecutor ProcessPoolExecutor
barato de iniciar, barato de compartir caro de iniciar, debe serializar para compartir
trabajo limitado por E/S trabajo limitado por CPU
Las colas (queue.Queue para hilos, multiprocessing.Queue para procesos) existen porque "aislado" y "compartido" todavía necesitan una forma segura de pasar datos a través del límite: una cola segura para hilos coordina el acceso dentro de la memoria compartida, mientras que una cola segura para procesos serializa realmente cada elemento a través del límite del proceso, haciendo más trabajo por elemento precisamente porque no hay memoria para compartir en primer lugar.
Consideraciones Avanzadas y Aplicaciones
Dos desarrollos más recientes en CPython relajan el predeterminado "un GIL, totalmente compartido" sin moverse completamente a procesos separados, y ambos se entienden mejor como nuevos puntos en el mismo espectro que conceptos completamente nuevos.
Los subintérpretes (utilizables desde Python a través de APIs relacionadas con PEP 734, 3.12+/3.13+) dan a cada intérprete su propio GIL y, por defecto, su propio estado aislado, mientras permanecen dentro de un único proceso del sistema operativo; un punto intermedio entre "un GIL compartido" y "procesos completamente separados", intercambiando parte del costo de inicio y comunicación entre procesos de multiprocessing por un aislamiento más ligero que un proceso completo, a costa de que la mayoría de los objetos aún no sean directamente compartibles a través del límite.
CPython con hilos libres (3.13+, experimental) adopta el enfoque opuesto: en lugar de aislar la memoria para evitar necesitar un bloqueo global único, reemplaza el GIL único con bloqueos más finos por objeto, de modo que los hilos realmente puedan ejecutar bytecode de Python en paralelo mientras continúan compartiendo memoria directamente; el mismo modelo de memoria compartida que el threading ordinario, pero con la seguridad previamente proporcionada por un gran bloqueo ahora distribuida en muchos más pequeños.
Ese único cambio es lo suficientemente significativo como para merecer una precaución real: las extensiones C escritas asumiendo que un GIL las protege implícitamente pueden no ser seguras bajo una compilación con hilos libres a menos que se actualicen para declarar y hacer cumplir su propia seguridad de hilos, que es por lo que adoptar una compilación sin GIL en producción significa auditar el árbol de dependencias, no solo cambiar un indicador de compilación.
| Modelo | Memoria | Mecanismo de seguridad | Mejor ajuste |
|---|---|---|---|
| Hilos (GIL predeterminado) | Totalmente compartido | Un GIL serializa la ejecución del bytecode | Trabajo limitado por E/S; compartir objetos de forma barata importa más que el paralelismo de CPU |
Procesos (multiprocessing) | Aislado por trabajador | Límite de proceso a nivel del SO; sin estado compartido por defecto | Trabajo limitado por CPU que necesita ejecución paralela real a través de núcleos |
| Subintérpretes | Aislado por intérprete, un proceso | GIL por intérprete | Aislar código no confiable o tipo plugin sin sobrecarga de proceso completo |
| Compilación con hilos libres | Totalmente compartido | Bloqueo por objeto en lugar de un bloqueo global único | Cargas de trabajo con hilos limitadas por CPU una vez que el árbol de dependencias se verifica como seguro para hilos |
La consecuencia práctica es que "qué herramienta de concurrencia debo usar" es realmente "¿cuánto aislamiento necesita esta carga de trabajo y cuánto puedo permitirme pagar por él?" - Elección de un Modelo de Concurrencia lo convierte en una lista de verificación concreta, pero la lista de verificación solo tiene sentido una vez que este intercambio entre memoria compartida y aislamiento es la lente a través de la cual lo estás viendo.
Conceptos Erróneos Comunes
- "Los hilos en Python en realidad no se ejecutan concurrentemente en absoluto." Se ejecutan concurrentemente; el GIL solo evita que dos hilos ejecuten bytecode de Python en el mismo instante exacto, pero las esperas de E/S, las extensiones C que liberan el GIL y la programación a nivel del sistema operativo todavía permiten que los hilos logren un progreso real superpuesto.
- "Multiprocessing es solo threading pero más pesado, sin ningún otro beneficio." La sobrecarga compra algo que el threading estructuralmente no puede bajo el GIL predeterminado: ejecución genuina y simultánea de bytecode de Python a través de núcleos de CPU.
- "Los subintérpretes son lo mismo que multiprocessing, solo que dentro de un proceso." Están aislados como los procesos en términos de estado predeterminado, pero comparten el proceso del SO y su espacio de memoria de una manera que los procesos separados de
multiprocessingno lo hacen, lo que cambia tanto el costo de inicio como lo que finalmente se puede compartir de forma segura entre ellos. - "Python con hilos libres elimina la necesidad de pensar en la seguridad de los hilos." Elimina el GIL único como mecanismo de seguridad, pero lo reemplaza con la necesidad de código y extensiones genuinamente seguros para hilos; la carga de seguridad se mueve, no desaparece.
- "
concurrent.futureses un tercer modelo de concurrencia, separado de hilos y procesos." Es una interfaz, no un modelo;ThreadPoolExecutoryProcessPoolExecutorson el mismo intercambio de hilos frente a procesos descrito aquí, envuelto en una API consistente.
Preguntas Frecuentes
¿Cuál es la única pregunta subyacente a la que responde cada herramienta de concurrencia de Python?
Cuánta memoria se comparte entre piezas de código que se ejecutan concurrentemente, y qué mecanismo mantiene segura esa memoria compartida: compartición completa bajo un bloqueo, o aislamiento completo sin necesidad de bloqueo.
¿Es el GIL una característica de rendimiento o una característica de seguridad?
Seguridad. Garantiza que solo un hilo ejecuta bytecode de Python a la vez, lo que mantiene las estructuras de objetos internas de CPython consistentes sin que cada operación necesite su propio bloqueo explícito; el intercambio de rendimiento (sin paralelismo de CPU entre hilos) es el costo de esa garantía de seguridad, no su propósito.
¿Por qué los hilos ayudan con las llamadas de red pero no con los bucles intensivos en CPU?
Las operaciones de E/S liberan el GIL mientras esperan, permitiendo que otro hilo se ejecute durante esa espera; un bucle de computación de Python ajustado nunca llega a un punto donde libere el GIL, por lo que otros hilos simplemente esperan su turno en lugar de ejecutarse en paralelo.
¿Por qué multiprocessing requiere serializar (pickling) datos entre procesos?
Porque cada proceso tiene su propio espacio de memoria separado y su propio intérprete; no hay memoria compartida que un proceso hijo pueda leer directamente del padre, por lo que cualquier dato que cruce ese límite debe convertirse a una forma transportable y reconstruirse en el otro lado.
¿Es `concurrent.futures` un modelo de concurrencia diferente de threading y multiprocessing?
No, es una interfaz Executor uniforme sobre los dos modelos subyacentes. ThreadPoolExecutor usa hilos y memoria compartida; ProcessPoolExecutor usa procesos y memoria aislada; cambiar entre ellos es principalmente un cambio de una línea.
¿Qué problema resuelven realmente los subintérpretes?
Ofrecen un aislamiento similar al de los procesos separados (cada uno con su propio GIL y, por defecto, su propio estado) mientras permanecen dentro de un único proceso del SO, lo que es más ligero que generar procesos completos para cargas de trabajo que principalmente necesitan aislamiento en lugar de acceso a memoria compartida.
¿Significa Python con hilos libres que los hilos ya no necesitan bloqueos?
No, elimina el bloqueo global único y lo reemplaza con bloqueos más finos por objeto, por lo que el acceso genuinamente concurrente a datos compartidos sigue siendo posible y el código (y las extensiones C) aún debe escribirse para ser seguro para hilos bajo ese modelo más fino.
¿Por qué no puedo usar siempre multiprocessing para estar seguro, ya que evita el GIL por completo?
Los procesos son más pesados de iniciar y requieren serializar datos a través del límite, lo que añade una sobrecarga real; para el trabajo limitado por E/S, donde el GIL no es la restricción en primer lugar, esa sobrecarga no compra nada y los hilos (o async) son el ajuste más barato.
¿Son las colas diferentes entre threading y multiprocessing?
Funcionalmente similares en la superficie, pero mecánicamente diferentes: queue.Queue coordina el acceso seguro a objetos compartidos en memoria entre hilos, mientras que multiprocessing.Queue serializa realmente cada elemento a través del límite del proceso, ya que no hay memoria para compartir directamente.
¿Es la adopción de una compilación con hilos libres un cambio directo para una base de código existente?
No de forma segura, sin auditar primero; las extensiones C escritas asumiendo que el GIL las protegía implícitamente pueden no ser seguras para hilos bajo una compilación donde ese bloqueo único ya no existe, por lo que verificar el árbol de dependencias es importante antes de confiar en él en producción.
¿Cómo debería decidir entre hilos y procesos para una tarea dada?
Pregunte si la carga de trabajo está limitada por E/S o por CPU: el trabajo limitado por E/S se beneficia del intercambio barato de los hilos ya que el GIL no es la restricción, mientras que el trabajo limitado por CPU necesita el paralelismo real que solo los procesos aislados (o una compilación verificada con hilos libres) pueden proporcionar.
¿Resuelven los subintérpretes y Python con hilos libres el mismo problema?
Abordan la misma limitación subyacente (un GIL compartido que serializa todo) desde direcciones opuestas: los subintérpretes añaden aislamiento para evitar compartir el bloqueo, mientras que los hilos libres eliminan el bloqueo único pero mantienen la memoria totalmente compartida.
Relacionados
- Fundamentos de Concurrencia - concurrencia frente a paralelismo y elección de una herramienta inicial
- El GIL y Python con Hilos Libres - el mecanismo GIL y la compilación sin GIL en profundidad
- multiprocessing - procesos aislados para paralelismo limitado por CPU
- threading - concurrencia de memoria compartida para trabajo limitado por E/S
- Subintérpretes - aislamiento por intérprete dentro de un proceso
- Elección de un Modelo de Concurrencia - convertir este intercambio en una lista de verificación de decisiones
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+ y uv 0.6+.