Aikido

Cómo evitar las condiciones de carrera: acceso seguro de los hilos al estado compartido

Riesgo de errores

Regla
Asegúrese de que hilo seguro acceso a compartido compartido.
Compartido mutable compartido accesible en a través de múltiples hilos
sin sincronización causa carrera conditions y tiempo de ejecución errores.

Lenguajes compatibles: Python, Java, C#

Introducción

Cuando varios hilos acceden y modifican variables compartidas sin sincronización, se producen condiciones de carrera. El valor final depende del tiempo de ejecución impredecible de los hilos, lo que lleva a la corrupción de datos, cálculos incorrectos o errores en tiempo de ejecución. Un contador incrementado por varios subprocesos sin bloqueo perderá actualizaciones cuando los subprocesos lean valores obsoletos, los incrementen y escriban resultados contradictorios.

Por qué es importante

Corrupción de datos y resultados incorrectos: Las condiciones de carrera provocan una corrupción silenciosa de los datos en la que los valores se vuelven incoherentes o incorrectos. Los saldos de las cuentas pueden ser erróneos, los recuentos de inventario pueden ser negativos o las estadísticas agregadas pueden estar corruptas. Estos errores son difíciles de reproducir porque dependen de la sincronización exacta de los hilos.

Inestabilidad del sistema: El acceso no sincronizado al estado compartido puede bloquear las aplicaciones. Un hilo puede modificar una estructura de datos mientras otro la lee, provocando excepciones como errores de puntero nulo o índices fuera de los límites. En producción, esto se manifiesta como fallos intermitentes bajo carga.

Complejidad de la depuración: Las condiciones de carrera son notoriamente difíciles de depurar porque no son deterministas. El fallo puede no aparecer en pruebas con un único hilo o en entornos de baja carga. La reproducción requiere un entrelazado de hilos específico que es difícil de forzar, lo que hace que los problemas aparezcan y desaparezcan aleatoriamente.

Ejemplos de códigos

❌ No conforme:

class CuentaBancaria:
    def  __init__(self):
 self.balance = 0

    def deposit(self, amount):
 current = self.balance
        # Condición de carrera: otro hilo puede modificar el saldo aquí
 time.sleep(0.001) # Simula el tiempo de procesamiento
 self.balance = current + amount

    def retirar(self, importe):
        if self.saldo >= importe: 
            current = self.balance
            time.sleep(0.001) 
            auto.saldo = actual - importe
            return True
        return False

Por qué está mal: Múltiples hilos llamando a depositar() o retirar() simultáneamente crean condiciones de carrera. Dos hilos depositando $100 cada uno podrían leer el saldo como $0, luego ambos escribir $100, resultando en un saldo final de $100 en lugar de $200.

✅ Conforme:

importar roscado

class BankAccount:
    def  __init__(self):
 self.__balance = 0
 self.__lock = threading.Lock()

   @property
    def balance(self):
        with self.__lock:
            return self.__balance

    def depósito(self, importe):
        con self.__lock: 
            current = self.__balance
            time.sleep(0.001) 
            self.__balance = current + amount

    def retirar(self, importe):
         con self.__lock:
            if self.__balance >= amount: 
                current = auto.__balance
                time.sleep(0.001) 
                auto.__balance = actual - importe
                return True
            return False

Por qué es importante: En threading.Lock() garantiza que sólo un hilo acceda al saldo a la vez. Cuando un hilo mantiene el bloqueo, los demás esperan, evitando modificaciones simultáneas. Privado __balance con sólo lectura @propiedad impide que un código externo anule la protección de la cerradura.

Conclusión

Proteja todo el estado mutable compartido con primitivas de sincronización apropiadas como bloqueos, semáforos u operaciones atómicas. Siempre que sea posible, prefiera estructuras de datos inmutables o almacenamiento local. Cuando la sincronización sea necesaria, minimice las secciones críticas para reducir la contención y mejorar el rendimiento.

Preguntas frecuentes

¿Tiene alguna pregunta?

¿Qué primitivas de sincronización debo utilizar?

Utilizar bloqueos (mutex) para el acceso exclusivo al estado compartido. Utilizar semáforos para limitar el acceso concurrente a los recursos. Utilice variables de condición para la coordinación y señalización de hilos. Para contadores simples o banderas, las operaciones atómicas son más rápidas que los bloqueos. Elija en función de su patrón de concurrencia: bloqueos para exclusión mutua, atómicos para operaciones simples, construcciones de nivel superior como colas para patrones productor-consumidor.

¿Cómo se evitan los bloqueos cuando se utilizan varios bloqueos?

Adquiera siempre los bloqueos en el mismo orden en todas las rutas de código. Si la función A necesita los bloqueos X e Y, y la función B necesita los bloqueos Y y X, adquiéralos en el mismo orden (siempre X y luego Y). Utilice la adquisición de bloqueos basada en el tiempo de espera para detectar posibles bloqueos. Mejor aún, rediseñe el código para que sólo necesite un bloqueo por sección crítica, o utilice estructuras de datos sin bloqueos.

¿Cuál es el impacto de la sincronización en el rendimiento?

La contención de bloqueos ralentiza el código altamente concurrente porque los hilos esperan a que los titulares de los bloqueos se liberen. Sin embargo, el código incorrecto no sincronizado es infinitamente más lento porque produce resultados erróneos. Minimice el alcance de los bloqueos (secciones críticas) para proteger únicamente la modificación del estado. Utilice bloqueos de lectura-escritura cuando varios lectores no entren en conflicto. Perfil antes de optimizar, la corrección es lo primero.

¿Puedo utilizar almacenamiento thread-local en lugar de bloqueos?

Sí, cuando cada subproceso necesita su propia copia de los datos. El almacenamiento local de subprocesos elimina la sobrecarga de sincronización al proporcionar a cada subproceso un estado privado. Se utiliza para cachés, búferes o acumuladores por subproceso que se fusionan posteriormente. Sin embargo, sigue siendo necesaria la sincronización cuando los subprocesos se comunican o comparten resultados finales.

¿Qué pasa con el Bloqueo Global del Intérprete (GIL) de Python?

El GIL no elimina la necesidad de bloqueos. Aunque evita la ejecución simultánea de código de bytes de Python, no hace que las operaciones sean atómicas. Un simple incremento de contador += 1 implica múltiples operaciones de bytecode donde el GIL puede ser liberado entre ellas. Utilice siempre la sincronización adecuada para el estado compartido, incluso en CPython.

¿Cómo compruebo las condiciones de carrera?

Utiliza sanitizadores de hilos y herramientas de pruebas de concurrencia específicas para tu lenguaje. Escriba pruebas de estrés que generen muchos subprocesos que realicen operaciones concurrentes y asegure que las invariantes se mantienen. Aumente el número de hilos y las iteraciones para exponer los errores dependientes del tiempo. Sin embargo, la superación de las pruebas no prueba la ausencia de condiciones de carrera, por lo que la revisión del código y un diseño cuidadoso de la sincronización siguen siendo fundamentales.

¿Qué son las estructuras de datos sin bloqueo y sin espera?

Las estructuras de datos sin bloqueos utilizan operaciones atómicas (comparar e intercambiar) en lugar de bloqueos, lo que garantiza el progreso de todo el sistema aunque los subprocesos se retrasen. Las estructuras sin esperas garantizan el progreso de cada subproceso. Son complejas de implementar correctamente, pero ofrecen un mejor rendimiento en condiciones de alta contención. Utilice bibliotecas de eficacia probada (java.util.concurrent, biblioteca atómica de C++) en lugar de implementar las suyas propias.

Asegúrese gratis

Proteja el código, la nube y el tiempo de ejecución en un sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No requiere tarjeta de crédito | Escanea resultados en 32segs.