En sistemas concurrentes, los bloqueos de exclusión mutua son fundamentales para garantizar la integridad de los recursos compartidos. Por ejemplo, en un entorno donde múltiples procesos acceden a una reserva compartida, se requiere un mecanismo para evitar condciiones de carrera al modificar los datos.
Bloqueos de Exclusión Mutua
Un bloqueo de exclusión mutua permite que solo un proceso o hilo ejecute una sección crítica a la vez. Considere un caso donde se decrementa un contador compartido usando multiprocessing:
from multiprocessing import Process, Lock
def reducir_contador(identificador, candado):
global contador_global
for iteracion in range(100):
candado.acquire()
contador_global -= 1
candado.release()
print(f'Proceso {identificador} completó la reducción.')
if __name__ == '__main__':
contador_global = 1000
candado = Lock()
lista_procesos = []
for idx in range(5):
proc = Process(target=reducir_contador, args=(idx, candado))
proc.start()
lista_procesos.append(proc)
for proc in lista_procesos:
proc.join()
print(f'Valor final del contador: {contador_global}')
Notas importantes sobre bloqueos: deben aplicarse solo en secciones críticas para evitar serialización innecesaria. El uso excesivo puede anular los beneficios de la concurrencia.
Interbloqueos
Los interbloqueos surgen cuando hilos o procesos se bloquean mutuamente, cada uno esperando un recurso que otro posee. Un escenario común involucra múltiples bloqueos adquiridos en orden inconsistente:
import threading
import tiempo
candado_alfa = threading.Lock()
candado_beta = threading.Lock()
def tarea_alfa():
candado_alfa.acquire()
print(f'{threading.current_thread().name} obtuvo candado alfa')
tiempo.sleep(0.2)
candado_beta.acquire()
print(f'{threading.current_thread().name} obtuvo candado beta')
candado_beta.release()
candado_alfa.release()
def tarea_beta():
candado_beta.acquire()
print(f'{threading.current_thread().name} obtuvo candado beta')
tiempo.sleep(0.2)
candado_alfa.acquire()
print(f'{threading.current_thread().name} obtuvo candado alfa')
candado_alfa.release()
candado_beta.release()
# Iniciar hilos
for _ in range(3):
hilo1 = threading.Thread(target=tarea_alfa)
hilo2 = threading.Thread(target=tarea_beta)
hilo1.start()
hilo2.start()
Este código puede provocar interbloqueos, donde todos los hilos quedan suspendidos indefinidamente.
Semáforos
Los semáforos controlan el acceso concurrente a un recurso limitando el número de hilos o procesos que pueden acceder simultáneamente. Por ejemplo, para gestionar un pool de conexiones:
import threading
import tiempo
sem = threading.Semaphore(2) # Limitar a 2 accesos concurrentes
def usar_conexion(id_hilo):
sem.acquire()
print(f'Hilo {id_hilo} está usando la conexión')
tiempo.sleep(1)
print(f'Hilo {id_hilo} liberó la conexión')
sem.release()
hilos = []
for i in range(5):
t = threading.Thread(target=usar_conexion, args=(i,))
hilos.append(t)
t.start()
for t in hilos:
t.join()
Cerrradura Global del Intérprete (GIL)
El GIL es un mutex en CPython que serializa la ejecución de hilos nativos, impidiendo el paralelismo real en múltiples núcleos. Existe porque la gestión de memoria de CPython no es segura para hilos, evitando que la recolección de basura cause errores.
Para verificar su existencia, se puede observar el comportamiento con operaciones atómicas:
import threading
valor_compartido = 0
def incrementar():
global valor_compartido
for _ in range(100000):
valor_compartido += 1
hilos = []
for _ in range(10):
h = threading.Thread(target=incrementar)
hilos.append(h)
h.start()
for h in hilos:
h.join()
print(f'Valor final: {valor_compartido}') # Resultado consistente gracias al GIL
El GIL no reemplaza a los bloqueos de aplicación para operaciones complejas. Por ejemplo, en una transacción bancaria:
import threading
import tiempo
saldo = 1000
def retirar(monto):
global saldo
temp = saldo
tiempo.sleep(0.01) # Simular demora
if temp >= monto:
saldo = temp - monto
print(f'Retiro exitoso, saldo: {saldo}')
else:
print('Fondos insuficientes')
hilos = []
for _ in range(5):
h = threading.Thread(target=retirar, args=(200,))
hilos.append(h)
h.start()
for h in hilos:
h.join()
print(f'Saldo final: {saldo}') # Puede variar sin bloqueo adicional
A pesar del GIL, el multihilo en Python sigue siendo útil para tareas intensivas en I/O, donde la conmutación de hilos puede mejorar el rendimiento. En cambio, para tareas intensivas en cómputo, el multiproceso aprovecha mejor los múltiples núcleos.
| Escenario | Análisis | Conclusión |
|---|---|---|
| Un núcleo, intensivo en I/O | Los hilos conmutan durante operaciones de E/S, con menor sobrecarga que los procesos. | Multihilo con ventaja. |
| Un núcleo, intensivo en cómputo | Conmutación similar, pero los procesos tienen mayor costo de creación. | Multihilo con ventaja. |
| Múltiples núcleos, intensivo en I/O | Las operaciones de E/S causan bloqueos frecuentes, reduciendo la ventaja del multiproceso. | Multihilo con ligera ventaja. |
| Múltiples núcleos, intensivo en cómputo | El GIL impide el paralelismo en hilos, por lo que los procesos usan múltiples núcleos eficientemente. | Multiproceso con gran ventaja. |