concurrencia vs paralelismo: Principal diferencia y casos de uso 2026
Resumen
La concurrencia y el paralelismo solucionan problemas diferentes. La concurrencia se trata de estructurar un programa para manejar muchas tareas que se superponen en el tiempo; el paralelismo se refiere a ejecutar literalmente múltiples cálculos al mismo instante en hardware separado.
En Python, threading y asyncio te brindan concurrencia sin núcleos de CPU adicionales. Ambos dependen de que una tarea ceda el control durante una espera de I/O en lugar de ejecutar genuinamente bytecode de Python al mismo tiempo que otro hilo.
El GIL impide que los hilos de Python paralelizen código limitado por CPU. Un benchmark en vivo en esta guía muestra que cuatro tareas limitadas por CPU tardan aproximadamente tanto en ejecutarse en hilos como en secuencia (5.45s vs. 5.22s), mientras que la multiprocesación redujo eso a 2.60s en dos núcleos.
Para trabajos limitados por I/O, threading y asyncio ofrecen la misma ventaja. El mismo benchmark muestra que ocho esperas de 0.5 segundos bajan de 4.00s en secuencia a 0.50s con ThreadPoolExecutor o asyncio.gather.
Python 3.13+ incluye una versión libre de hilos oficialmente soportada que desactiva el GIL — la primera vez que los hilos de CPython pueden genuinamente paralelizar código limitado por CPU, con la advertencia de que algunos paquetes de extensiones en C aún obligan a activar el GIL.
La elección correcta depende de la tarea, no de una preferencia. El trabajo limitado por CPU necesita multiprocesamiento (o una versión libre de hilos) para un verdadero paralelismo; el trabajo limitado por I/O obtiene plena concurrencia de threading o asyncio sin necesidad de más núcleos en absoluto.
Concurrencia vs. paralelismo: las definiciones reales
La concurrencia es una forma de estructurar un programa de modo que múltiples tareas puedan estar en progreso al mismo tiempo, incluso si solo una de ellas se está ejecutando realmente en un instante dado; el paralelismo son múltiples cálculos ejecutándose físicamente al mismo instante en unidades de procesamiento separadas. La formulación de Rob Pike, de una charla organizada en el propio blog de ingeniería de Go, traza la línea precisamente: la concurrencia es la composición de procesos que se ejecutan independientemente, mientras que el paralelismo es la ejecución simultánea de cálculos — la concurrencia se trata de lidiar con muchas cosas a la vez, el paralelismo es sobre hacer muchas cosas a la vez.
La analogía clásica se mantiene bien: un cajero que cambia entre tres clientes — escaneando un artículo para el cliente A, respondiendo a una pregunta del cliente B, empacando comestibles para el cliente C — es concurrencia. Tres cajeros, cada uno dedicado por completo a un cliente en el mismo momento, es paralelismo. Una máquina de un solo núcleo puede ejecutar un programa altamente concurrente (el sistema operativo entrelaza muchos hilos) sin lograr nunca el paralelismo, y una máquina de múltiples núcleos puede ejecutar código paralelo que no es concurrente en absoluto (cuatro cálculos independientes y no superpuestos que simplemente se ejecutan en cuatro núcleos de manera secuencial). Las dos propiedades son independientes entre sí, no son dos puntos en la misma escala.
Donde Python traza la misma línea: threading, multiprocesamiento y asyncio
La documentación de la biblioteca estándar de Python organiza sus herramientas de concurrencia en torno a exactamente esta separación: "la elección adecuada de la herramienta dependerá de la tarea a ejecutar (limitada por CPU vs limitada por I/O)." threading y asyncio brindan a un programa Python concurrencia — muchas tareas parecen avanzar a la vez — sin requerir más de un núcleo de CPU. multiprocessing es lo que le da a un programa Python un verdadero paralelismo, porque ejecuta procesos del sistema operativo separados, cada uno con su propio intérprete de Python y espacio de memoria, en núcleos separados.
La razón por la que el threading por sí solo no puede convertirse en paralelismo para trabajos limitados por CPU es el Global Interpreter Lock: la propia documentación de CPython afirma claramente que "solo un hilo puede ejecutar código de Python a la vez", y recomienda multiprocessing o concurrent.futures.ProcessPoolExecutor para cargas de trabajo limitadas por CPU en máquinas de múltiples núcleos, mientras que señala que "el threading sigue siendo un modelo apropiado si deseas ejecutar múltiples tareas limitadas por I/O simultáneamente." Esa única oración es todo el marco de decisión para CPython clásico (habilitado por GIL), y el benchmark a continuación muestra exactamente por qué.
import time
import threading
import multiprocessing
defcpu_task(n): total =0for i inrange(n): total += i * i
return total
N =20_000_000WORKERS =4defrun_threaded(): threads =[threading.Thread(target=cpu_task, args=(N,))for _ inrange(WORKERS)]for t in threads: t.start()for t in threads: t.join()defrun_multiprocessing():with multiprocessing.Pool(processes=WORKERS)as pool: pool.map(cpu_task,[N]* WORKERS)
Ejecutar en una máquina de 2 núcleos, cuatro tareas limitadas por la CPU (20 millones de iteraciones de bucle cada una) tomó 5.22 segundos ejecutándose una tras otra, 5.45 segundos ejecutándose en cuatro hilos (sin mejora — si acaso, ligeramente peor por la sobrecarga del cambio de hilos), y 2.60 segundos ejecutándose en un grupo de procesos de 4 trabajadores, coincidiendo aproximadamente con los 2 núcleos físicos disponibles. El uso de hilos agregó concurrencia (cuatro tareas técnicamente en vuelo) sin agregar paralelismo (nada realmente terminó más rápido), que es el efecto documentado del GIL en la práctica, no solo en teoría.
Echa un vistazo rápido
La concurrencia arregla cuán rápido tu código emite solicitudes, pero un trabajo de scraping o monitoreo que lanza cientos de solicitudes concurrentes desde una IP solo es limitado por la tasa más rápido — el gateway residencial rotativo de Nstproxy distribuye ese mismo tráfico concurrente a través de un grupo de IPs de salida en lugar de una sola.
El trabajo limitado por I/O cuenta la historia opuesta, porque una espera bloqueante — una lectura de socket, un time.sleep, un viaje de ida y vuelta a la base de datos — libera el GIL sin importar si el código está en hilos o en modo asíncrono. El siguiente benchmark simula esa espera sin depender de acceso a red externo, ya que un sueño bloqueante y una lectura de socket bloqueante liberan el control de la misma manera:
import time
import asyncio
from concurrent.futures import ThreadPoolExecutor
TAREAS =8RETARDO =0.5deftarea_io(): time.sleep(RETARDO)asyncdeftarea_io_asincrona():await asyncio.sleep(RETARDO)defejecutar_hilos():with ThreadPoolExecutor(max_workers=TAREAS)as pool:list(pool.map(lambda_: tarea_io(),range(TAREAS)))asyncdefejecutar_principal_asincrono():await asyncio.gather(*(tarea_io_asincrona()for _ inrange(TAREAS)))
Ocho esperas de 0.5 segundos tomaron 4.00 segundos al ejecutarse secuencialmente, y 0.50 segundos al ejecutarse ya sea en hilos o con asyncio.gather — ambos enfoques lograron la aceleración completa de 8x disponible, porque ninguna de las ocho tareas necesitaba la CPU mientras estaban esperando. Esta es la razón práctica por la que la mayor parte del código de red en Python (scraping web, sondeos API, grupos de solicitudes respaldados por proxies) recurre a hilos o asyncio en lugar de multiprocesamiento: el cuello de botella es el viaje de ida y vuelta a la red, no la CPU, por lo que no hay trabajo limitado por CPU para que los procesos adicionales lo paralelicen.
¿Libera Python 3.13+ el hilo libre de cambios en la regla?
A partir de Python 3.13, CPython admite el hilo libre, una variante de construcción oficialmente soportada donde el GIL está deshabilitado por defecto, y ese soporte continúa en 3.14 — la primera vez en la historia de CPython que los hilos han podido ejecutar realmente bytecode de Python en paralelo. Sin embargo, no es la construcción predeterminada: los instaladores estándar todavía distribuyen el intérprete habilitado para el GIL, y una construcción de hilo libre puede reenabilitar el GIL en tiempo de ejecución a través de PYTHON_GIL=1 o python -X gil=1. La guía oficial de hilo libre también señala directamente el compromiso actual: "algunos paquetes de terceros, en particular aquellos con un módulo de extensión, pueden no estar listos para usarse en una construcción de hilo libre, y volverán a habilitar el GIL," lo que significa que una biblioteca construida contra la API C clásica puede silenciosamente devolver un programa de hilo libre a un territorio limitado por el GIL. Para 2026, la conclusión práctica es que la regla de que el trabajo limitado por CPU necesita multiprocesamiento todavía se mantiene para la gran mayoría de las aplicaciones de producción de Python que ejecutan la construcción estándar, pero ya no es un límite arquitectónico permanente: los equipos que realizan trabajos numéricos intensivos en CPU y pesados en hilos tienen una alternativa real (aunque aún en desarrollo) a evaluar.
Costos y compromisos operativos
Los hilos y tareas asíncronas comparten el espacio de memoria de un proceso, lo que mantiene bajo su sobrecosto — iniciar un hilo cuesta una fracción de la memoria que necesita un proceso completo del sistema operativo — pero esa memoria compartida es también lo que hace que el código en hilos sea propenso a condiciones de carrera en cualquier objeto que más de un hilo muta, y por qué depurar un error de concurrencia es más difícil que depurar uno de línea recta: el fallo depende del tiempo, no solo de la entrada. Los procesos evitan ese peligro de memoria compartida porque cada uno obtiene su propio intérprete y espacio de memoria, pero esa aislación es exactamente por la que multiprocessing cuesta más para comenzar y más para comunicarse — pasar datos entre procesos significa serializarlos (a través de pickle por defecto), no solo entregar una referencia.
El código asíncrono evita completamente la sobrecarga de los hilos del sistema operativo (una coroutine de Python es mucho más ligera que un hilo del sistema operativo), razón por la cual los servidores basados en asyncio pueden mantener abiertas muchas más conexiones concurrentes que un diseño de un hilo por conexión del mismo tamaño, pero esa eficiencia viene con una regla propia: una sola llamada bloqueante y no asíncrona dentro de una función async def detiene todo el bucle de eventos, no solo esa tarea, ya que en primer lugar solo hay un hilo ejecutando el bucle de eventos. El tamaño del pool es importante para los tres: un ThreadPoolExecutor sin límite o un pool de procesos puede agotar la memoria o los descriptores de archivos bajo carga real tan fácilmente como puede subparalelizar una carga de trabajo que es demasiado pequeña para necesitarlo, por lo que el tamaño del pool debe seguir el verdadero cuello de botella (núcleos de CPU para multiprocessing, un techo de concurrencia probado para hilos y tareas asíncronas) en lugar de un valor predeterminado arbitrario.
Análisis de escenarios: donde cada uno realmente gana
Un trabajo restringido por la CPU —redimensionamiento de imágenes a través de un lote de archivos, simulación numérica, análisis y transformación de un gran conjunto de datos en memoria— es el escenario del paralelismo: más núcleos realizando la misma cantidad fija de trabajo lo finalizan más rápido, y multiprocessing.Pool o concurrent.futures.ProcessPoolExecutor son la herramienta de la biblioteca estándar para ello. Un trabajo restringido por I/O —llamadas a una docena de API, lectura de muchos archivos, o emisión de solicitudes HTTP concurrentes contra un sitio objetivo— es el escenario de la concurrencia: el cuello de botella está en esperar algo externo, no en calcular algo, por lo que asyncio o un ThreadPoolExecutor alcanzan el mismo techo que añadir más núcleos de CPU nunca lograría.
Las cargas de trabajo pesadas de Python en la red —raspado, monitoreo de precios, verificación de anuncios, sondeos masivos de API— se sitúan claramente en la segunda categoría, y ahí es donde una solución puramente a nivel de código se encuentra con un límite no relacionado con el código: un sitio objetivo o API no percibe "un programa emitiendo solicitudes concurrentes", ve tantas solicitudes por segundo como llegan desde una IP determinada, y la mayoría de los sitios limitan o bloquean exactamente en esa base independientemente de cuán eficientemente esté estructurado el código del cliente. La concurrencia controla cuán rápido puede emitir solicitudes un programa de Python; no tiene influencia sobre cuántas IPs fuente distintas parecen provenir esas solicitudes. Ese es un eje separado que finalmente un pipeline de raspado o monitoreo tiene que resolver: aceptar un techo de tasa por IP, o enrutar solicitudes concurrentes a través de un pool de IPs para que el tráfico no se concentre en una sola dirección.
Nstproxy es un proveedor de infraestructura proxy construido para ese segundo eje: una puerta de enlace proxy residencial que distribuye solicitudes salientes a través de un gran pool de IPs en lugar de una sola dirección, dirigido a equipos cuyo código de concurrencia en Python ya es eficiente pero aún queda por debajo de los límites de tasa por IP. Su línea Residential Lite se adapta a un equipo que simplemente está añadiendo rotación de IP a un raspador concurrente existente: paquetes prepagos que comienzan en 10GB por $10 (aproximadamente $1.00/GB), respaldados por un pool que el proveedor afirma tener más de 50 millones de IPs residenciales en más de 200 países y regiones con una tasa de éxito del 99.5%, sin renovación automática de suscripción que gestionar. El sacrificio que vale la pena conocer antes de adoptarlo: Residential Lite está fijado para trabajos sensibles al rendimiento y costos, no para latencias de solicitudes individuales, por lo que una carga de trabajo que necesita la respuesta más rápida posible —en lugar del mayor volumen concurrente sostenible— debería considerar eso en comparación con una línea residencial o de centro de datos premium.
Una puerta de enlace rotativa, no una lista de IPs autogestionada — un host:puerto fijo con rotación del lado del servidor elimina el mantenimiento de la lista de IPs que de otro modo estaría al lado del propio código de concurrencia.
Escala con el nivel de concurrencia ya presente en el código — dado que la puerta de enlace rota independientemente del modelo de threading/asyncio del cliente, añadir más trabajadores concurrentes no requiere un aprovisionamiento adicional de lógica de infraestructura de proxy.
Soporte para HTTP, HTTPS y SOCKS5 — funciona con los mismos patrones de configuración de proxy requests/aiohttp utilizados en cualquiera de los enfoques de concurrencia anteriores, por lo que cambiar los modelos de concurrencia no requiere cambiar el código de integración del proxy.
Guía de decisiones
Pregunta qué está esperando realmente el trabajo. Si la respuesta es "la CPU, calculando algo", el paralelismo es la palanca: utiliza multiprocessing o ProcessPoolExecutor, dimensiona el grupo según el número de núcleos físicos y espera una aceleración casi lineal hasta ese límite. Si la respuesta es "una respuesta externa — una llamada de red, una lectura de disco, otro servicio", la concurrencia es la palanca: utiliza asyncio para un gran número de tareas ligeras, principalmente de red, o threading/ThreadPoolExecutor cuando el código que llama a bibliotecas de bloqueo no asíncronas no puede reescribirse alrededor de await. Si una carga de trabajo tiene genuinamente tanto una etapa intensa en CPU como una etapa intensa en I/O — por ejemplo, analizar un cuerpo de respuesta grande después de obtenerlo — combinar asyncio para la etapa de obtención con un ProcessPoolExecutor para la etapa de análisis es un patrón documentado y soportado en lugar de una solución alternativa.
Conclusión
La concurrencia y el paralelismo responden a diferentes preguntas: cómo está estructurado un programa para manejar trabajos superpuestos, en contraste con cuántas computaciones se ejecutan físicamente al mismo instante — y la biblioteca estándar de Python mantiene esa distinción intacta: threading/asyncio para concurrencia, multiprocessing para paralelismo, con el GIL como la razón específica y documentada por la que el clásico CPython necesita esa separación. Las métricas anteriores muestran que la separación no es teórica: las mismas cuatro tareas que no obtuvieron ningún beneficio de threading cayeron a menos de la mitad del tiempo de ejecución bajo multiprocessing, y las mismas ocho esperas de I/O obtuvieron el beneficio completo ya sea de threading o de asyncio sin tocar un segundo núcleo.
P: ¿Es asyncio de Python concurrencia o paralelismo?
Asyncio es concurrencia, no paralelismo — un bucle de eventos de un solo hilo ejecuta una corutina a la vez y cambia a otra cada vez que la actual espera una operación de I/O, por lo que muchas tareas progresan en ventanas de tiempo superpuestas sin que ninguna de ellas ejecute código Python al mismo instante literal.
P: ¿El multiprocessing utiliza más memoria que threading?
Sí — cada proceso generado por multiprocessing obtiene su propio intérprete de Python y espacio de memoria, lo que cuesta sustancialmente más que un hilo (que comparte la memoria de su proceso padre), y ese overhead es la razón por la que el multiprocessing merece su costo para trabajos limitados por CPU, pero rara vez para trabajos limitados por I/O que threading o asyncio ya manejan de manera eficiente.
P: ¿Se puede tener paralelismo sin concurrencia, o concurrencia sin paralelismo?
Sí a ambos — cuatro trabajos por lotes independientes y no superpuestos que se ejecutan uno tras otro en cuatro núcleos diferentes es paralelismo sin concurrencia (nada se superpone en el tiempo aunque se utilicen múltiples núcleos), y un programa altamente concurrente de un solo núcleo que entrelaza cientos de hilos es concurrencia sin paralelismo (nada se ejecuta al mismo instante literal).
P: ¿El free-threading de Python 3.13+ elimina la limitación del GIL?
Parcialmente — el free-threading es una construcción de CPython oficialmente soportada y no por defecto (que continúa en 3.14) que desactiva el GIL y permite que los hilos se ejecuten genuinamente en paralelo, pero no es el instalador por defecto, puede ser reactivado en tiempo de ejecución, y algunos paquetes de extensión en C aún fuerzan el GIL de vuelta, por lo que la regla clásica de que threading no puede paralelizar el trabajo de CPU sigue aplicándose a la mayoría de los Python de producción hoy en día.
P: ¿Debo usar threading o asyncio para un raspador web en Python?
Ambos funcionan para la recolección pura limitada por I/O y las métricas anteriores muestran que funcionan prácticamente igual; asyncio tiende a escalar a un mayor número de conexiones concurrentes con menos overhead, mientras que threading a menudo es más simple de adoptar cuando el raspador ya depende de bibliotecas sincrónicas y no asíncronas.
P: ¿Por qué mi raspador concurrente sigue siendo limitado por tasa a pesar de cambiar a asyncio?
Porque la concurrencia solo controla qué tan rápido emite tu programa solicitudes, no cuántas direcciones IP distintas parecen provenir esas solicitudes — un sitio objetivo que limita la tasa por IP de origen restringirá a un cliente asíncrono rápido exactamente como lo haría con uno secuencial lento, a menos que las solicitudes también se distribuyan a través de múltiples IPs salientes.
Marcus Chen
Aug. 6th 2026
110M+ IP reales con 99.9% de acceso exitoso
Respuesta media ultrarrapida ~0.5s para tareas de alta concurrencia
Desde solo $0.1/GB
Acceso inmediato a pools premium de proxies residenciales, datacenter, IPv6 e ISP.