Técnicas prácticas de sincronización de subprocesos (threads)
Ahora que hemos cubierto los conceptos básicos de la sincronización de datos compartidos en subprocesos (threads) de Python, exploremos algunos ejemplos prácticos de cómo utilizar las diversas primitivas de sincronización.
Uso de locks (candados)
Los locks son la primitiva de sincronización más básica en Python. Aseguran que solo un subproceso (thread) puede acceder a una sección crítica de código a la vez. Aquí tienes un ejemplo de cómo utilizar un lock para proteger un contador compartido:
import threading
counter = 0
lock = threading.Lock()
def increment_counter():
global counter
for _ in range(1000000):
with lock:
counter += 1
threads = []
for _ in range(2):
t = threading.Thread(target=increment_counter)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Final counter value: {counter}")
En este ejemplo, el objeto lock
se utiliza para asegurarse de que solo un subproceso (thread) puede acceder a la sección crítica de código donde se incrementa la variable counter
.
Uso de semáforos (semaphores)
Los semáforos se utilizan para controlar el acceso a un número limitado de recursos. Aquí tienes un ejemplo de cómo utilizar un semáforo para limitar el número de conexiones concurrentes a una base de datos:
import threading
import time
database_connections = 3
connection_semaphore = threading.Semaphore(database_connections)
def use_database():
with connection_semaphore:
print(f"{threading.current_thread().name} acquired a database connection.")
time.sleep(2) ## Simulating database operation
print(f"{threading.current_thread().name} released a database connection.")
threads = []
for _ in range(5):
t = threading.Thread(target=use_database)
threads.append(t)
t.start()
for t in threads:
t.join()
En este ejemplo, el connection_semaphore
se utiliza para limitar el número de conexiones concurrentes a la base de datos a 3. Cada subproceso (thread) debe adquirir un "permiso" del semáforo antes de poder utilizar una conexión a la base de datos.
Uso de variables de condición (condition variables)
Las variables de condición permiten que los subprocesos (threads) esperen a que se cumplan ciertas condiciones antes de continuar su ejecución. Aquí tienes un ejemplo de cómo utilizar una variable de condición para coordinar la producción y el consumo de elementos en una cola:
import threading
import time
queue = []
queue_size = 5
queue_condition = threading.Condition()
def producer():
with queue_condition:
while len(queue) == queue_size:
queue_condition.wait()
queue.append(1)
print(f"{threading.current_thread().name} produced an item. Queue size: {len(queue)}")
queue_condition.notify_all()
def consumer():
with queue_condition:
while not queue:
queue_condition.wait()
item = queue.pop(0)
print(f"{threading.current_thread().name} consumed an item. Queue size: {len(queue)}")
queue_condition.notify_all()
producer_threads = [threading.Thread(target=producer) for _ in range(2)]
consumer_threads = [threading.Thread(target=consumer) for _ in range(3)]
for t in producer_threads + consumer_threads:
t.start()
for t in producer_threads + consumer_threads:
t.join()
En este ejemplo, la variable queue_condition
se utiliza para coordinar la producción y el consumo de elementos en una cola. Los productores esperan a que la cola tenga espacio disponible, mientras que los consumidores esperan a que la cola tenga elementos.
Estos ejemplos demuestran cómo se pueden utilizar las diversas primitivas de sincronización proporcionadas por el módulo threading
de Python para gestionar eficazmente los recursos compartidos y evitar problemas comunes de concurrencia.