Imagina que estás trabajando en una aplicación que necesita procesar múltiples tareas al mismo tiempo: desde manejar solicitudes web hasta realizar cálculos complejos de manera simultánea. A medida que las aplicaciones se vuelven más exigentes, las soluciones básicas de concurrencia ya no son suficientes. Aquí es donde entran las herramientas avanzadas de Python como threading y multiprocessing, que te permiten sacar el máximo provecho de tu CPU y gestionar tareas de manera eficiente y sin errores.
En esta clase, aprenderás a manejar escenarios más complicados, como evitar que los hilos interfieran entre sí, compartir datos de manera segura entre procesos y prevenir bloqueos que puedan detener tu aplicación. Prepárate para llevar la programación concurrente y paralela a un nivel más profesional y resolver problemas que los desarrolladores enfrentan en proyectos del mundo real.
1. Sincronización de Hilos en Python
Cuando varios hilos intentan acceder a un mismo recurso al mismo tiempo, pueden ocurrir problemas de coherencia. Para evitar esto, se utilizan mecanismos de sincronización, como Lock y RLock, que garantizan que solo un hilo acceda a un recurso crítico a la vez.
Ejemplo: Uso de Lock para Evitar Condiciones de Carrera
import threading
# Variable compartidasaldo =0lock = threading.Lock()# Crear un Lockdefdepositar(dinero):global saldo
for _ inrange(100000):with lock:# Bloquear el acceso para evitar condiciones de carrera saldo += dinero
hilos =[]for _ inrange(2): hilo = threading.Thread(target=depositar, args=(1,)) hilos.append(hilo) hilo.start()for hilo in hilos: hilo.join()print(f"Saldo final: {saldo}")# Esperamos ver 200000 como saldo
Explicación:
El uso de Lock asegura que solo un hilo modifique la variable saldo en un momento dado, evitando que el resultado final sea incorrecto.
2. Compartir Datos entre Procesos con multiprocessing
A diferencia de los hilos, los procesos no comparten memoria de forma predeterminada. Para que dos procesos puedan compartir datos, Python proporciona herramientas como multiprocessing.Queue y multiprocessing.Value.
Ejemplo: Compartir Datos con Queue en multiprocessing
import multiprocessing
defcalcular_cuadrado(numeros, cola):for n in numeros: cola.put(n * n)if __name__ =="__main__": numeros =[1,2,3,4,5] cola = multiprocessing.Queue() proceso = multiprocessing.Process(target=calcular_cuadrado, args=(numeros, cola)) proceso.start() proceso.join()# Extraer resultados de la colawhilenot cola.empty():print(cola.get())
Explicación:
Usamos Queue para que el proceso secundario pueda pasar datos de vuelta al proceso principal.
3. Problemas de Sincronización y Cómo Evitarlos
A medida que manejas tareas más complejas, es posible que te encuentres con problemas como deadlocks y race conditions. Entender estos problemas es crucial para escribir código concurrente robusto.
Evitar Deadlocks con RLock
Un deadlock ocurre cuando dos o más hilos se bloquean mutuamente al esperar por un recurso que está siendo utilizado por otro hilo. Para evitar esto, podemos usar RLock en lugar de Lock.
Usamos RLock para evitar que múltiples operaciones simultáneas en una cuenta causen bloqueos.
4. Coordinación de Tareas con multiprocessing.Manager
Cuando los procesos deben compartir estructuras de datos complejas (como listas o diccionarios), podemos usar un Manager para crear un espacio de memoria compartido entre procesos.
Ejemplo: Uso de Manager para Compartir Listas entre Procesos
multiprocessing.Manager nos permite crear una lista compartida entre varios procesos, facilitando la comunicación entre ellos.
¡Lo lograste! Ahora tienes en tus manos poderosas técnicas para manejar múltiples tareas de forma eficiente. Aprendiste a sincronizar hilos para evitar errores, a compartir datos de manera segura entre procesos y a evitar bloqueos que podrían detener tus aplicaciones. Todo esto te prepara para enfrentar los desafíos del desarrollo de software moderno, donde la concurrencia y el paralelismo son esenciales para crear aplicaciones rápidas, eficientes y escalables.
Con estas herramientas avanzadas, tu código no solo será más rápido, sino también más robusto y confiable. Este es el tipo de conocimiento que te permite destacar en proyectos grandes y complejos. ¡Estás listo para aplicar todo lo que has aprendido y optimizar tus próximas creaciones en Python!
No entiendo como las clases importantes son un texto, absurdo.
Y luego mal explicada
me estaba costando entender el ejemplo de multi procesing pues no encontraba cual era el proceso principal y cual el secundario, si alguien mas tuvo este problema aca la explicacion que me hizo entender.
En el código que has compartido, distinguimos entre el proceso principal y el proceso secundario de la siguiente manera:
### **Proceso Principal**
El proceso principal es aquel que inicia y maneja todo el programa. En este caso, el proceso principal es el que ejecuta el bloque de código dentro de if \_\_name\_\_ == "\_\_main\_\_":. Es responsable de:
- Definir la lista numeros.
- Crear la cola cola para la comunicación entre procesos.
- Crear y empezar el proceso secundario.
- Esperar a que el proceso secundario termine.
- Extraer y mostrar los resultados de la cola.
### **Proceso Secundario**
El proceso secundario es el que se crea específicamente para ejecutar una tarea en paralelo al proceso principal. En este caso, el proceso secundario se define con la línea proceso = multiprocessing.Process(target=calcular\_cuadrado, args=(numeros, cola)) y se inicia con proceso.start(). Este proceso secundario:
- Ejecuta la función calcular\_cuadrado(numeros, cola).
- Realiza el cálculo de los cuadrados de los números en la lista numeros.
- Inserta los resultados en la cola cola.
### **Explicación de la Cola (Queue)**
La cola Queue se utiliza para permitir la comunicación entre el proceso secundario y el proceso principal. El proceso secundario coloca los resultados de los cálculos en la cola, y luego el proceso principal extrae esos resultados para mostrarlos.
### **Flujo del Código**
1. **Proceso Principal**: Define la lista numeros y crea la cola cola.
2. **Proceso Principal**: Crea el proceso secundario con la función objetivo calcular\_cuadrado.
3. **Proceso Principal**: Inicia el proceso secundario.
4. **Proceso Secundario**: Ejecuta la función calcular\_cuadrado, calcula los cuadrados y coloca los resultados en la cola.
5. **Proceso Principal**: Espera a que el proceso secundario termine con proceso.join().
6. **Proceso Principal**: Extrae los resultados de la cola y los imprime.
Este esquema permite que el proceso secundario haga cálculos de manera independiente, mientras que el proceso principal sigue manejando otras tareas y eventualmente recopila los resultados.
by copilot
Gracias por tu aporte
No se entiende nada, lo generó con chatGPT, el texto, el codigo y la imagen!!
jajaja pero es que creo que el curso entero está hecho con chat GPT. Ojo! No me parece mal, yo hubiera hecho lo mismo, incluso tiene la ventaja de que si le pasas el índice de temas del curso a chat GPT y le pedis que te haga guias con cada tema, podes hacerle preguntas y es más interactivo. Después ponés los videos del curso como para fijar el conocimiento, tipo repaso. Los ejemplos que se usan son prácticamente los mismos. Es más! Creo que en la clase que se explicó lo del if __name__ == "__main__" para controlar la ejecución del código en los módulos, faltó grabar la parte en la que chat GPT pasa el codigo a módulos para mostrar que lo que está abajo del "if name..." no se ejecuta.
El curso puede ser mucho mejor. Pudo haberse dividido en unos 3 cursos distintos y de niveles de tecnicismos y dificultad.
Para que se tenga en cuenta. De todas formas, gracias por el tiempo y conocimiento que brindan al público
si tambien intentaron imprimir antes del join y no les imprimio nada Antes del join: El proceso secundario aún no ha terminado, por lo que la cola podría estar vacía.
Después del join: El proceso secundario ha terminado, y la cola contiene los elementos que el proceso secundario ha puesto.
El método join() es crucial para sincronizar el proceso principal con el proceso secundario, asegurando que el proceso secundario haya completado su tarea antes de que el proceso principal continúe.
Muchas gracias por tu aporte
Me ha tocado investigar por mi propia cuenta, pero hay temas que no se pueden resumir en un video de 3 minutos o en un texto, más sin embargo debemos entender que estos temas sin ninguna duda, serian un curso aparte, lo que aqui se muestra es un abre bocas de lo que es, asi que para mi no esta mal.
Los que no entienden es que no han puesto en practica estos conocimientos, fin.
weeee!!
Paralelismo con multiprocessing
El módulo multiprocessing permite ejecutar múltiples procesos en diferentes núcleos de CPU, lo que es ideal para tareas CPU-bound, como cálculos intensivos.
import multiprocessing
import time
deftarea(nombre):print(f"Inicio de {nombre}") time.sleep(2)print(f"Fin de {nombre}")# Crear dos procesosproceso1 = multiprocessing.Process(target=tarea, args=("Proceso 1",))proceso2 = multiprocessing.Process(target=tarea, args=("Proceso 2",))# Iniciar los procesosproceso1.start()proceso2.start()# Esperar a que los procesos terminenproceso1.join()proceso2.join()print("Todas las tareas han terminado.")
Las condiciones de carrera son uno de los problemas más críticos en programación concurrente, especialmente cuando se trabaja con hilos (threading) o procesos (multiprocessing) en Python.
🧠 ¿Qué son las condiciones de carrera?
Una condición de carrera ocurre cuando dos o más hilos o procesos acceden simultáneamente a un recurso compartido (como una variable, archivo o estructura de datos), y al menos uno de ellos modifica ese recurso. El resultado final depende del orden en que se ejecutan las operaciones, lo cual puede ser impredecible y causar errores difíciles de detectar.
🔍 Ejemplo clásico
Supongamos que dos hilos intentan incrementar una misma variable global:
contador =0def incrementar(): global contador
for _ inrange(1000): contador +=1
Si ejecutás esta función en varios hilos sin protección, el valor final de contador puede ser incorrecto porque las operaciones de lectura y escritura se intercalan de forma no controlada.
🛡️ ¿Cómo se previenen?
En Python, se usan primitivas de sincronización como:
Lock: bloquea el acceso a una sección crítica para que solo un hilo la ejecute a la vez.
RLock: una versión reentrante del Lock.
Semaphore: útil para controlar acceso a recursos limitados.
Condition: permite que los hilos esperen hasta que se cumpla una condición específica.
Ejemplo con Lock:
import threading
contador =0lock = threading.Lock()def incrementar(): global contador
for _ inrange(1000):withlock: contador +=1
Este patrón asegura que solo un hilo pueda modificar contador en cada momento, evitando condiciones de carrera.
El uso de hilos en Python permite ejecutar tareas simultáneamente, mejorando la eficiencia en comparación con la ejecución en cascada. Aquí tienes un ejemplo que compara ambas formas:
Ejecución en Cascada:
import time
deftarea(): time.sleep(1)# Simula una tarea que toma tiempostart = time.time()for _ inrange(5): tarea()end = time.time()print(f"Tiempo en cascada: {end - start} segundos")
Uso de Hilos:
import threading
import time
deftarea(): time.sleep(1)# Simula una tarea que toma tiempostart = time.time()hilos =[]for _ inrange(5): hilo = threading.Thread(target=tarea) hilos.append(hilo) hilo.start()for hilo in hilos: hilo.join()end = time.time()print(f"Tiempo con hilos: {end - start} segundos")
La ejecución en cascada tomará alrededor de 5 segundos, mientras que con hilos debería tomar cerca de 1 segundo, mostrando así la eficiencia del uso de hilos para tareas concurrentes.
multiprocessing.Manager() sirve para crear un es un servidor de objetos compartidos que van mas alla de listas:
lista: manager.list()
Diccionario: manager.dict()
Cola: manager.Queue()
Namespace: manager.Namespace()
Valor: manager.Value(type, val)
Lock: manager.Lock()
Primera ves que no entedí :(
vez*
import threading
import multiprocessing
# Variable compartida
saldo =0lock = threading.Lock() # Crear un Lockdef depositar(dinero): global saldo
for _ inrange(100000):withlock: # Bloquear el acceso para evitar condiciones de carrera
saldo += dinero
hilos =[]for _ inrange(2): hilo = threading.Thread(target=depositar, args=(1,)) hilos.append(hilo) hilo.start()for hilo inhilos: hilo.join()print(f"Saldo final: {saldo}") # Esperamos ver 200000 como saldo
def calcular_cuadrado(numeros, cola):for n innumeros: cola.put(n * n)if __name__ =="__main__": numeros =[1,2,3,4,5] cola = multiprocessing.Queue() proceso = multiprocessing.Process(target=calcular_cuadrado, args=(numeros, cola)) proceso.start() proceso.join() # Extraer resultados de la cola
while not cola.empty():print(cola.get())classCuentaBancaria: def __init__(self, saldo): self.saldo= saldo
self.lock= threading.RLock() def transferir(self, otra_cuenta, cantidad):with self.lock: self.saldo-= cantidad
otra_cuenta.depositar(cantidad) def depositar(self, cantidad):with self.lock: self.saldo+= cantidad
cuenta1 =CuentaBancaria(500)cuenta2 =CuentaBancaria(300)hilo1 = threading.Thread(target=cuenta1.transferir, args=(cuenta2,200))hilo2 = threading.Thread(target=cuenta2.transferir, args=(cuenta1,100))hilo1.start()hilo2.start()hilo1.join()hilo2.join()print(f"Saldo cuenta1: {cuenta1.saldo}")print(f"Saldo cuenta2: {cuenta2.saldo}")def agregar_valores(lista_compartida):for i inrange(5): lista_compartida.append(i)if __name__ =="__main__":with multiprocessing.Manager()asmanager: lista_compartida = manager.list() proceso1 = multiprocessing.Process(target=agregar_valores, args=(lista_compartida,)) proceso2 = multiprocessing.Process(target=agregar_valores, args=(lista_compartida,)) proceso1.start() proceso2.start() proceso1.join() proceso2.join()print(f"Lista compartida: {lista_compartida}")
En Python, las bibliotecas threading y multiprocessing permiten ejecutar múltiples tareas en paralelo, pero tienen diferencias importantes. **threading** se usa principalmente para tareas de entrada/salida (I/O), como redes o acceso a archivos, debido a la limitación del **Global Interpreter Lock (GIL)**. **multiprocessing**, por otro lado, permite ejecutar código en múltiples procesos y es más adecuado para tareas que consumen mucha CPU, ya que evita el GIL.
A continuación, detallo cada biblioteca y su uso.
### 1. threading: Concurrencia a través de hilos
La biblioteca threading permite la creación y manejo de hilos en Python. Los hilos comparten el mismo espacio de memoria y recursos, lo que facilita la comunicación entre ellos, pero limita su uso en tareas intensivas en CPU debido al GIL.
#### Uso básico de threading
import threading
deftarea(nombre):  print(f"Iniciando {nombre}")  \# Simulación de trabajo  for i in range(3):  print(f"{nombre} ejecutando {i}")  print(f"Terminando {nombre}")
\# Crear hilos
hilo1 = threading.Thread(target=tarea, args=("Hilo 1",))hilo2 = threading.Thread(target=tarea, args=("Hilo 2",))
\# Iniciar hilos
hilo1.start()hilo2.start()
\# Esperar a que terminen
hilo1.join()hilo2.join()
#### Ventajas y Limitaciones de threading
- **Ventajas**: Ideal para tareas de entrada/salida que pueden esperar (ej., operaciones de red, archivos).
- **Limitaciones**: Debido al GIL, solo un hilo puede ejecutar bytecode de Python a la vez, lo que limita la utilidad de threading para operaciones que consumen mucha CPU.
### 2. multiprocessing: Paralelismo con múltiples procesos
La biblioteca multiprocessing permite la ejecución de tareas en múltiples procesos, cada uno con su propio intérprete de Python, evitando el GIL. Esto es útil para tareas que requieren cálculos intensivos, como el procesamiento de datos.
#### Uso básico de multiprocessing
from multiprocessing import Process
deftarea(nombre):  print(f"Iniciando {nombre}")  \# Simulación de trabajo  for i in range(3):  print(f"{nombre} ejecutando {i}")  print(f"Terminando {nombre}")
\# Crear procesos
proceso1 = Process(target=tarea, args=("Proceso 1",))proceso2 = Process(target=tarea, args=("Proceso 2",))
\# Iniciar procesos
proceso1.start()proceso2.start()
\# Esperar a que terminen
proceso1.join()proceso2.join()
#### Ventajas y Limitaciones de multiprocessing
- **Ventajas**: Permite un verdadero paralelismo, útil para tareas CPU intensivas. Cada proceso tiene su propio espacio de memoria y no está afectado por el GIL.
- **Limitaciones**: Cada proceso consume más memoria y tiene más sobrecarga en la comunicación entre procesos que los hilos.
### Comunicación entre Hilos y Procesos
#### threading: Comunicación a través de variables compartidas
En threading, los hilos pueden compartir variables y recursos de la misma clase, ya que todos operan en el mismo espacio de memoria. Es importante utilizar **bloqueos (locks)** para evitar problemas de sincronización.
import threading
contador =0bloqueo = threading.Lock()defincrementar():  global contador  for \_ in range(1000):  with bloqueo: # Bloqueo para evitar condiciones de carrera  contador += 1
\# Crear hilos
hilos = \[threading.Thread(target=incrementar)for \_ inrange(5)]
\# Iniciar hilos
for hilo in hilos:  hilo.start()
\# Esperar a que terminen
for hilo in hilos:  hilo.join()print(f"Contador final: {contador}")
#### multiprocessing: Comunicación a través de colas y pipes
Dado que los procesos no comparten memoria, multiprocessing proporciona colas (Queue) y pipes (Pipe) para la comunicación.
from multiprocessing import Process, Queue
defproductor(q):  for i in range(5):  q.put(i) # Añadir elementos a la cola  print(f"Producto {i} añadido a la cola")defconsumidor(q):  while not q.empty():  item = q.get() # Obtener elementos de la cola  print(f"Producto {item} consumido")cola = Queue()
\# Crear procesos
p1 = Process(target=productor, args=(cola,))p2 = Process(target=consumidor, args=(cola,))
\# Iniciar procesos
p1.start()p2.start()
\# Esperar a que terminen
p1.join()p2.join()
| Tareas CPU intensivas (procesamiento de datos)| multiprocessing |
| Necesidad de comunicación sencilla entre tareas | threading con variables compartidas y locks |
| Necesidad de aislamiento de datos | multiprocessing con Queue o Pipe |
### Resumen
- **threading** es ideal para tareas concurrentes y basadas en I/O donde el GIL no es un problema.
- **multiprocessing** permite el verdadero paralelismo y es mejor para tareas intensivas en CPU.
- La **comunicación** entre hilos se hace mediante variables compartidas y bloqueos, mientras que en multiprocessing se utilizan Queue y Pipe para compartir datos entre procesos.
Ambas bibliotecas son útiles para optimizar el rendimiento en Python, pero elegir la correcta depende del tipo de tarea y del diseño de la aplicación.