Los mecanismos de alto nivel para concurrencia en Python proporcionan abstracciones que facilitan el manejo seguro de múltiples hilos sin tener que gestionar bloqueos de bajo nivel manualmente. A continuación, exploramos algunos de los mecanismos más importantes, con ejemplos prácticos para ilustrar su uso.
Colecciones concurrentes
Las colecciones concurrentes en Python, como las implementadas en el módulo queue
, son estructuras de datos que permiten el acceso seguro desde múltiples hilos sin necesidad de sincronización explícita. Estas colecciones están diseñadas para evitar condiciones de carrera y garantizar el acceso coordinado.
Ejemplo: queue.Queue
queue.Queue
es una de las colecciones concurrentes más comunes en Python. Esta clase permite a los hilos añadir y retirar elementos de forma segura, sin necesidad de utilizar locks manuales.
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 26 27 28 29 30 |
import threading import queue cola = queue.Queue() def productor(): for i in range(10): print(f"Produciendo {i}") cola.put(i) def consumidor(): while True: item = cola.get() if item is None: break print(f"Consumiendo {item}") cola.task_done() # Crear y ejecutar hilos hilo_productor = threading.Thread(target=productor) hilo_consumidor = threading.Thread(target=consumidor) hilo_productor.start() hilo_consumidor.start() hilo_productor.join() # Finalizar el consumidor cola.put(None) hilo_consumidor.join() |
En este ejemplo, el productor agrega elementos a la cola, y el consumidor los extrae. queue.Queue
se encarga automáticamente de la sincronización entre ambos hilos.
Variables atómicas
Las variables atómicas permiten realizar operaciones sobre variables compartidas entre hilos de manera segura y atómica (sin interrupción). En Python, este concepto se puede emular utilizando primitivas de sincronización como el Lock
o el módulo atomic
(disponible a través de bibliotecas externas), pero también se pueden utilizar tipos de datos inmutables para garantizar la seguridad de las operaciones.
Ejemplo: Uso del módulo threading
para emular variables atómicas
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 26 27 28 29 30 31 |
import threading class VariableAtomica: def __init__(self, valor_inicial=0): self.valor = valor_inicial self.lock = threading.Lock() def incrementar(self, cantidad): with self.lock: # Exclusión mutua self.valor += cantidad def obtener(self): with self.lock: return self.valor contador_atomico = VariableAtomica() def incrementar_contador(): for _ in range(100000): contador_atomico.incrementar(1) hilo1 = threading.Thread(target=incrementar_contador) hilo2 = threading.Thread(target=incrementar_contador) hilo1.start() hilo2.start() hilo1.join() hilo2.join() print(f"Valor final del contador atómico: {contador_atomico.obtener()}") |
En este ejemplo, la clase VariableAtomica
utiliza un Lock
para asegurar que las operaciones sobre el valor compartido se realicen de forma segura y atómica.
Números aleatorios en contexto multihilo
Los generadores de números aleatorios también pueden ser un punto de conflicto en programas multihilo si no se manejan adecuadamente. En Python, el módulo random
no es seguro para su uso en múltiples hilos si se comparte el mismo estado entre hilos. Sin embargo, se puede crear una instancia separada de random.Random
para cada hilo, lo que evita interferencias.
Ejemplo: Números aleatorios en entornos concurrentes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import threading import random def generar_aleatorios(nombre): random_gen = random.Random() # Instancia separada para cada hilo for _ in range(5): print(f"{nombre} generó: {random_gen.randint(1, 100)}") hilo1 = threading.Thread(target=generar_aleatorios, args=("Hilo 1",)) hilo2 = threading.Thread(target=generar_aleatorios, args=("Hilo 2",)) hilo1.start() hilo2.start() hilo1.join() hilo2.join() |
En este ejemplo, cada hilo utiliza su propia instancia de random.Random()
, lo que garantiza que no haya problemas de concurrencia al generar números aleatorios.