Cómo sincronizar recursos compartidos en subprocesos (threads) de Python

PythonPythonBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Las capacidades de subprocesamiento (threading) de Python permiten a los desarrolladores aprovechar el poder del procesamiento paralelo, pero administrar los recursos compartidos entre subprocesos (threads) puede ser una tarea desafiante. Este tutorial lo guiará a través del proceso de sincronización de datos compartidos en subprocesos (threads) de Python, asegurando una ejecución segura para subprocesos (thread-safe) e integridad de los datos.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") python/ErrorandExceptionHandlingGroup -.-> python/finally_block("Finally Block") python/AdvancedTopicsGroup -.-> python/context_managers("Context Managers") python/AdvancedTopicsGroup -.-> python/threading_multiprocessing("Multithreading and Multiprocessing") subgraph Lab Skills python/catching_exceptions -.-> lab-417457{{"Cómo sincronizar recursos compartidos en subprocesos (threads) de Python"}} python/raising_exceptions -.-> lab-417457{{"Cómo sincronizar recursos compartidos en subprocesos (threads) de Python"}} python/finally_block -.-> lab-417457{{"Cómo sincronizar recursos compartidos en subprocesos (threads) de Python"}} python/context_managers -.-> lab-417457{{"Cómo sincronizar recursos compartidos en subprocesos (threads) de Python"}} python/threading_multiprocessing -.-> lab-417457{{"Cómo sincronizar recursos compartidos en subprocesos (threads) de Python"}} end

Presentación de los subprocesos (threads) de Python

El módulo de subprocesamiento (threading) incorporado en Python te permite crear y administrar subprocesos (threads), que son unidades ligeras de ejecución que pueden ejecutarse de forma concurrente dentro de un solo proceso. Los subprocesos (threads) son útiles cuando necesitas realizar múltiples tareas simultáneamente, como manejar operaciones de E/S, procesar datos en segundo plano o responder a múltiples solicitudes de clientes.

¿Qué son los subprocesos (threads) de Python?

Los subprocesos (threads) son secuencias de ejecución independientes dentro de un solo proceso. Comparten el mismo espacio de memoria, lo que significa que pueden acceder y modificar las mismas variables y estructuras de datos. Este acceso compartido a los recursos puede dar lugar a problemas de sincronización, que discutiremos en la siguiente sección.

Beneficios de usar subprocesos (threads)

El uso de subprocesos (threads) en Python puede ofrecer varios beneficios, entre ellos:

  1. Mejor respuesta: Los subprocesos (threads) permiten que tu aplicación siga respondiendo mientras realiza tareas que consumen mucho tiempo, como operaciones de E/S o cálculos de larga duración.
  2. Paralelismo: Los subprocesos (threads) pueden aprovechar los procesadores multinúcleo para ejecutar tareas de forma concurrente, lo que puede mejorar el rendimiento general de tu aplicación.
  3. Compartición de recursos: Los subprocesos (threads) dentro del mismo proceso pueden compartir datos y recursos, lo que puede ser más eficiente que crear procesos separados.

Posibles desafíos con los subprocesos (threads)

Si bien los subprocesos (threads) pueden ser poderosos, también presentan algunos desafíos de los que debes estar al tanto:

  1. Sincronización: Cuando múltiples subprocesos (threads) acceden a recursos compartidos, debes asegurarte de que no interfieran en las operaciones de los demás, lo que puede dar lugar a condiciones de carrera y otros problemas de sincronización.
  2. Interbloqueos (Deadlocks): La gestión inadecuada de los recursos compartidos puede resultar en interbloqueos (deadlocks), donde dos o más subprocesos (threads) esperan a que los demás liberen recursos, lo que hace que la aplicación deje de responder.
  3. Seguridad para subprocesos (Thread Safety): Debes asegurarte de que tu código sea seguro para subprocesos (thread-safe), es decir, que pueda ejecutarse de forma segura por múltiples subprocesos (threads) sin causar corrupción de datos u otros problemas.

En la siguiente sección, profundizaremos en el tema de la sincronización de recursos compartidos en los subprocesos (threads) de Python.

Sincronización de datos compartidos

Cuando múltiples subprocesos (threads) acceden a los mismos recursos compartidos, como variables o estructuras de datos, puede dar lugar a condiciones de carrera y otros problemas de sincronización. Estos problemas pueden causar corrupción de datos, comportamientos inesperados o incluso fallos en tu aplicación. Para abordar estos problemas, Python proporciona varios mecanismos para sincronizar datos compartidos.

Condiciones de carrera

Una condición de carrera se produce cuando el resultado final de un cálculo depende del tiempo relativo o de la intercalación de las operaciones de múltiples subprocesos (threads) en datos compartidos. Esto puede llevar a resultados impredecibles e incorrectos.

Considere el siguiente ejemplo:

import threading

counter = 0

def increment_counter():
    global counter
    for _ in range(1000000):
        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, dos subprocesos (threads) están incrementando una variable counter compartida 1.000.000 de veces cada uno. Sin embargo, debido a las condiciones de carrera, el valor final de counter puede no ser 2.000.000 como se esperaba.

Primitivas de sincronización

El módulo threading de Python proporciona varias primitivas de sincronización para ayudarte a gestionar el acceso a recursos compartidos:

  1. Locks (Candados): Los locks son la primitiva de sincronización más básica. Te permiten asegurarte de que solo un subproceso (thread) puede acceder a una sección crítica de código a la vez.
  2. Semáforos (Semaphores): Los semáforos se utilizan para controlar el acceso a un número limitado de recursos.
  3. 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.
  4. Eventos (Events): Los eventos se utilizan para señalar a uno o más subprocesos (threads) que se ha producido un evento en particular.

Estas primitivas de sincronización se pueden utilizar para asegurarte de que tus subprocesos (threads) accedan a los recursos compartidos de manera segura y coordinada, evitando condiciones de carrera y otros problemas de sincronización.

graph LR A[Thread 1] --> B[Acquire Lock] B --> C[Critical Section] C --> D[Release Lock] E[Thread 2] --> F[Acquire Lock] F --> G[Critical Section] G --> H[Release Lock]

En la siguiente sección, exploraremos ejemplos prácticos de cómo utilizar estas técnicas de sincronización en tus aplicaciones de Python.

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.

Resumen

En este completo tutorial de Python, aprenderás cómo sincronizar eficazmente los recursos compartidos en tus aplicaciones multihilo. Al comprender las primitivas de sincronización incorporadas en Python, como los locks (candados), los semáforos y las variables de condición, podrás coordinar el acceso concurrente y evitar las condiciones de carrera, asegurando la estabilidad y confiabilidad de tus programas de Python.