Contenidos
La sincronización de hilos es fundamental para evitar errores en programas multihilo, como condiciones de carrera o interbloqueos. A continuación, exploramos varios conceptos clave relacionados con la sincronización de hilos en Python, con ejemplos prácticos.
Exclusión mutua: condiciones de carrera y secciones críticas
Una condición de carrera ocurre cuando varios hilos acceden y modifican un recurso compartido sin coordinación adecuada, lo que genera resultados impredecibles. Para prevenir esto, se utilizan secciones críticas, que son fragmentos de código donde solo un hilo puede acceder a un recurso compartido a la vez.
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import threading contador = 0 lock = threading.Lock() # Exclusión mutua mediante un lock def incrementar(): global contador for _ in range(100000): with lock: # Sección crítica protegida contador += 1 # Crear y ejecutar hilos hilos = [threading.Thread(target=incrementar) for _ in range(2)] for hilo in hilos: hilo.start() for hilo in hilos: hilo.join() print(f"Valor final del contador: {contador}") |
Aquí, usamos un lock para proteger la sección crítica, asegurando que solo un hilo modifique el contador a la vez, evitando una condición de carrera.
Bloqueo intrínseco: bloques de código sincronizados
En Python, los bloqueos intrínsecos (también conocidos como locks) permiten a los hilos sincronizarse fácilmente. Cuando un hilo adquiere un lock, ningún otro hilo puede entrar en la sección bloqueada hasta que el primero libere el lock.
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import threading recurso = 0 lock = threading.Lock() def modificar_recurso(): global recurso lock.acquire() # Bloqueo try: recurso += 1 print(f"Recurso actualizado a {recurso}") finally: lock.release() # Liberación del bloqueo hilo1 = threading.Thread(target=modificar_recurso) hilo2 = threading.Thread(target=modificar_recurso) hilo1.start() hilo2.start() hilo1.join() hilo2.join() |
En este ejemplo, el lock.acquire()
y lock.release()
aseguran que el recurso sea modificado por un solo hilo a la vez, previniendo interferencias entre hilos.
Compartición de recursos: interbloqueo
Un interbloqueo ocurre cuando dos o más hilos esperan indefinidamente que otro hilo libere un recurso, generando una situación donde ninguno puede continuar. Esto se puede evitar implementando medidas como la evitación de recursos circulares o utilizando timeouts.
Ejemplo de interbloqueo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import threading lock_a = threading.Lock() lock_b = threading.Lock() def hilo_1(): with lock_a: print("Hilo 1 adquirió lock A") with lock_b: print("Hilo 1 adquirió lock B") def hilo_2(): with lock_b: print("Hilo 2 adquirió lock B") with lock_a: print("Hilo 2 adquirió lock A") t1 = threading.Thread(target=hilo_1) t2 = threading.Thread(target=hilo_2) t1.start() t2.start() t1.join() t2.join() |
En este código, ambos hilos intentan adquirir dos locks en diferente orden, lo que puede causar un interbloqueo si no se maneja adecuadamente.
Compartición de recursos con bloqueo dependiente de su estado
Para evitar interbloqueos, es importante diseñar los bloqueos de tal manera que los hilos no esperen indefinidamente si un recurso no está disponible. Esto se puede hacer utilizando bloqueos con timeouts.
Ejemplo con timeout:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import threading import time lock = threading.Lock() def tarea(): print("Intentando adquirir el lock...") if lock.acquire(timeout=1): # Espera 1 segundo para adquirir el lock try: print("Lock adquirido, procesando...") time.sleep(2) finally: lock.release() else: print("No se pudo adquirir el lock, tarea no completada.") t1 = threading.Thread(target=tarea) t2 = threading.Thread(target=tarea) t1.start() t2.start() t1.join() t2.join() |
Aquí, el segundo hilo puede fallar en adquirir el lock si el primer hilo lo mantiene por más de 1 segundo, evitando un potencial interbloqueo.
Clases thread-safe
Las clases thread-safe son aquellas que permiten el uso seguro en entornos multihilo sin la necesidad de sincronización explícita por parte del usuario. Un ejemplo de esto en Python es la clase Queue
, que permite a los hilos agregar y eliminar elementos de manera segura.
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import threading import queue q = queue.Queue() def productor(): for i in range(5): print(f"Produciendo {i}") q.put(i) def consumidor(): while not q.empty(): item = q.get() print(f"Consumiendo {item}") q.task_done() hilo_productor = threading.Thread(target=productor) hilo_consumidor = threading.Thread(target=consumidor) hilo_productor.start() hilo_productor.join() hilo_consumidor.start() hilo_consumidor.join() |
Aquí, queue.Queue
es una clase thread-safe que maneja automáticamente la sincronización de los hilos productores y consumidores.