- Bloqueo Mutuo (Mutex) – Implementación Práctica
Un mutex (bloqueo mutuo) garantiza que solo un hilo acceda a los datos compartidos en un momento dado. Los hilos comptien por el bloqueo; el que lo obtiene ejecuta el código crítico, y los demás esperan hasta que se libere.
from multiprocessing import Process, Lock
import time
import json
import random
def consultar(nombre):
with open('datos.json', 'r', encoding='utf8') as f:
info = json.load(f)
print(f'{nombre} consulta: {info.get("ticket_num")} restantes')
def comprar(nombre):
with open('datos.json', 'r', encoding='utf8') as f:
info = json.load(f)
time.sleep(random.randint(1, 3))
if info.get('ticket_num') > 0:
with open('datos.json', 'w', encoding='utf8') as f:
info['ticket_num'] -= 1
json.dump(info, f)
print(f'{nombre} compró exitosamente')
else:
print(f'{nombre} falló, no hay boletos')
def ejecutar(nombre, candado):
consultar(nombre)
candado.acquire()
comprar(nombre)
candado.release()
if __name__ == '__main__':
candado = Lock()
for i in range(10):
p = Process(target=ejecutar, args=(f'Usuario {i}', candado))
p.start()
Punto muerto (deadlock)
Ocurre cuando un hilo retiene el bloqueo A y espera el B, mientras otro retiene B y espera A. Ambos quedan bloqueados indefinidamente.
from threading import Thread, Lock
import time
candadoA = Lock()
candadoB = Lock()
class MiHilo(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
candadoA.acquire()
print(f'{self.name} tomó A')
candadoB.acquire()
print(f'{self.name} tomó B')
candadoB.release()
candadoA.release()
def func2(self):
candadoB.acquire()
print(f'{self.name} tomó B')
time.sleep(1)
candadoA.acquire()
print(f'{self.name} tomó A')
candadoA.release()
candadoB.release()
for i in range(10):
h = MiHilo()
h.start()
- Teoría de Hilos
Proceso: unidad de recursos (espacio de memoria). Un archivo Python en ejecución es un proceso.
Hilo: unidad de ejecución. Dentro de cada proceso hay al menos un hilo que ejecuta las instrucciones.
- Los hilos comparten memoria y recursos del proceso padre.
- La creación de hilos es más liviana que la de procesos.
- Permiten concurrencia y comunicación sencilla mediante variables globales.
- Creación de Hilos
Similar a procesos: mediante función o clase.
from threading import Thread
import time
def tarea(nombre):
print(f'{nombre} ejecutándose')
time.sleep(0.1)
print(f'{nombre} terminó')
if __name__ == '__main__':
lista_hilos = []
for i in range(100):
t = Thread(target=tarea, args=(f'Usuario {i}',))
t.start()
lista_hilos.append(t)
for t in lista_hilos:
t.join()
print('Hilo principal finalizó')
class MiHilo(Thread):
def run(self):
print('Ejecutando run')
time.sleep(1)
print('Run terminó')
obj = MiHilo()
obj.start()
print('Hilo principal')
- GIL (Global Interpreter Lock)
En CPython, el GIL es un mutex que impide que múltiples hilos ejecuten bytecode al mismo tiempo. Existe porque la gestión de memoria de CPython no es segura para hilos (recolección de basura).
- El GIL hace que los hilos se ejecuten de forma cuasi‑secuencial (solo uno activo a la vez).
- Para tareas intensivas en CPU, un solo proceso con muchos hilos puede ser más lento que múltiples procesos.
- Para tareas intensivas en E/S, los hilos siguen siendo ventajosos porque ceden el GIL durante las esperas.
import time
from threading import Thread, Lock
contador = 100
candado = Lock()
def restar(candado):
global contador
candado.acquire()
temp = contador
time.sleep(0.1)
contador = temp - 1
candado.release()
hilos = []
for _ in range(100):
t = Thread(target=restar, args=(candado,))
t.start()
hilos.append(t)
for t in hilos:
t.join()
print(contador) # Debe ser 0
- Semáforo
Un semáforo permite que un número fijo de hilos accedan a un recurso simultáneamente. Funciona como un conjunto de múltiples mutex.
from threading import Thread, Semaphore
import time
import random
semaforo = Semaphore(5)
class Tarea(Thread):
def run(self):
semaforo.acquire()
print(f'{self.name} adquirió semáforo')
time.sleep(random.randint(1, 3))
semaforo.release()
for i in range(20):
t = Tarea()
t.start()
- Eventos
Un objeto Event permite sincronización: los hilos esperan hasta que se active el evento, luego continúan.
from threading import Thread, Event
import time
evento = Event()
def semaforo_luz():
print('Rojo: todos detenidos')
time.sleep(3)
print('Verde: ¡aceleren!')
evento.set()
def auto(nombre):
print(f'{nombre} esperando luz verde')
evento.wait()
print(f'{nombre} arranca')
h = Thread(target=semaforo_luz)
h.start()
for i in range(20):
t = Thread(target=auto, args=(f'Auto {i}',))
t.start()
- Pool de Procesos y Pool de Hilos
Crear procesos/hilos sin límite puede saturar el sistema. Los pools mantienen un número fijo de workers y reutilizan los ya creados.
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import random
def trabajo(n):
print(f'Trabajando en {n}')
time.sleep(random.randint(1, 2))
return n ** 2
if __name__ == '__main__':
# Pool de procesos (5 workers)
with ProcessPoolExecutor(max_workers=5) as pool:
futures = [pool.submit(trabajo, i) for i in range(10)]
for f in futures:
print('Resultado:', f.result())
# Pool de hilos (10 workers)
with ThreadPoolExecutor(max_workers=10) as pool:
futures = [pool.submit(trabajo, i) for i in range(10)]
for f in futures:
print('Resultado:', f.result())
Se pueden definir callbacks con add_done_callback:
def callback(fut):
print('Callback:', fut.result())
with ThreadPoolExecutor() as pool:
fut = pool.submit(lambda x: x*2, 21)
fut.add_done_callback(callback)
- Corrutinas (Concurrencia en un solo hilo)
Las corrutinas permiten concurrencia dentro de un solo hilo mediante la detección de operaciones de E/S y el cambio de contexto manual. La biblioteca gevent implementa esto.
import time
from gevent import monkey; monkey.patch_all()
from gevent import spawn
def funcion1():
print('Inicio f1')
time.sleep(3)
print('Fin f1')
def funcion2():
print('Inicio f2')
time.sleep(5)
print('Fin f2')
start = time.time()
t1 = spawn(funcion1)
t2 = spawn(funcion2)
t1.join()
t2.join()
print('Tiempo:', time.time() - start) # Aprox 5s (no 8s)
- Corrutinas para concurrencia real (servidor)
import socket
from gevent import monkey; monkey.patch_all()
from gevent import spawn
def atender(conn):
while True:
data = conn.recv(1024)
if not data:
break
print('Recibido:', data.decode())
conn.send(data.upper())
conn.close()
def servidor():
s = socket.socket()
s.bind(('127.0.0.1', 8080))
s.listen(5)
while True:
conn, addr = s.accept()
spawn(atender, conn)
if __name__ == '__main__':
spawn(servidor).join()
Combniación óptima: usar múltiples procesos, dentro de cada proceso múltiples hilos, y dentro de cada hilo corrutinas para máxima eficiencia.