Aikido

Comment éviter les conditions de course : accès à l'état partagé sans risque pour les threads

Risque de bogue

Règle
Assurer l'accès l'accès aux partagé partagé.
Les états partagés mutable mutable accessible par plusieurs threads
sans synchronisation provoque course conditions et d'exécution d'exécution.

Langages pris en charge : Python, Java, C#

Introduction

Lorsque plusieurs threads accèdent à des variables partagées et les modifient sans synchronisation, des conditions de course se produisent. La valeur finale dépend de la synchronisation imprévisible de l'exécution des threads, ce qui entraîne une corruption des données, des calculs incorrects ou des erreurs d'exécution. Un compteur incrémenté par plusieurs threads sans verrouillage manquera des mises à jour lorsque les threads liront des valeurs périmées, les incrémenteront et écriront des résultats contradictoires.

Pourquoi c'est important

Corruption des données et résultats incorrects : Les conditions de course provoquent une corruption silencieuse des données où les valeurs deviennent incohérentes ou incorrectes. Les soldes des comptes peuvent être erronés, les inventaires peuvent être négatifs ou les statistiques agrégées peuvent être corrompues. Ces bogues sont difficiles à reproduire car ils dépendent de la synchronisation exacte des threads.

Instabilité du système : L'accès non synchronisé à l'état partagé peut faire planter les applications. Un thread peut modifier une structure de données pendant qu'un autre la lit, ce qui provoque des exceptions telles que des erreurs de pointeur nul ou des index hors limites. En production, ces exceptions se manifestent par des pannes intermittentes sous charge.

Complexité du débogage : Les conditions de course sont notoirement difficiles à déboguer parce qu'elles ne sont pas déterministes. Le bogue peut ne pas apparaître dans les tests à un seul thread ou dans les environnements à faible charge. La reproduction nécessite un entrelacement spécifique des threads qui est difficile à forcer, ce qui fait que les problèmes apparaissent et disparaissent de manière aléatoire.

Exemples de code

❌ Non conforme :

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

    def deposit(self, amount) :
 current = self.balance
        # Condition de course : un autre thread peut modifier le solde ici
 time.sleep(0.001) # Simule le temps de traitement
 self.balance = current + amount

    def withdraw(self, amount) :
        if self.balance >= amount : 
            current = self.balance
            time.sleep(0.001) 
            self.balance = current - amount
            return True
        return False

Pourquoi ce n'est pas correct : Plusieurs threads appelant simultanément deposit() ou withdraw() créent des conditions de course. Deux threads déposant chacun 100 $ peuvent lire le solde à 0 $, puis écrire tous les deux 100 $, ce qui donne un solde final de 100 $ au lieu de 200 $.

✅ Conforme :

importer le filage

classe Compte bancaire:
    def  __init__(self):
 self.__balance = 0
 self.__lock = threading.Lock()

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

    def deposit(self, amount) :
        with self.__lock : 
            current = self.__balance
            time.sleep(0.001) 
            self.__balance = current + amount

    def withdraw(self, amount) :
        with self.__lock :
            si self.__balance >= amount : 
                current = self.__balance
                time.sleep(0.001) 
                self.__balance = current - amount
                return True
            return False

Pourquoi cela est-il important ? Le threading.Lock() garantit qu'un seul thread accède à l'équilibre à la fois. Lorsqu'un thread détient le verrou, les autres attendent, ce qui empêche les modifications simultanées. Privé Équilibre avec lecture seule @propriété empêche le code externe de contourner la protection de la serrure.

Conclusion

Protéger tous les états mutables partagés à l'aide de primitives de synchronisation appropriées telles que les verrous, les sémaphores ou les opérations atomiques. Préférez les structures de données immuables ou le stockage local des threads lorsque cela est possible. Lorsque la synchronisation est nécessaire, minimisez les sections critiques afin de réduire les conflits et d'améliorer les performances.

FAQ

Vous avez des questions ?

Quelles primitives de synchronisation dois-je utiliser ?

Utiliser des verrous (mutex) pour un accès exclusif à un état partagé. Utiliser des sémaphores pour limiter l'accès concurrent aux ressources. Utiliser des variables de condition pour la coordination et la signalisation des threads. Pour les simples compteurs ou drapeaux, les opérations atomiques sont plus rapides que les verrous. Choisissez en fonction de votre modèle de concurrence : les verrous pour l'exclusion mutuelle, les opérations atomiques pour les opérations simples, les constructions de plus haut niveau comme les files d'attente pour les modèles producteur-consommateur.

Comment éviter les blocages lors de l'utilisation de plusieurs verrous ?

Acquérir les verrous dans le même ordre pour tous les chemins de code. Si la fonction A a besoin des verrous X et Y, et que la fonction B a besoin des verrous Y et X, acquérez-les dans un ordre cohérent (toujours X puis Y). Utilisez l'acquisition de verrous basée sur le délai d'attente pour détecter les blocages potentiels. Mieux encore, revoir la conception afin de n'avoir besoin que d'un seul verrou par section critique, ou utiliser des structures de données sans verrou.

Quel est l'impact de la synchronisation sur les performances ?

La contention des verrous ralentit les codes hautement concurrents parce que les threads attendent que les détenteurs de verrous se libèrent. Toutefois, un code non synchronisé incorrect est infiniment plus lent parce qu'il produit des résultats erronés. Réduire la portée des verrous (sections critiques) pour ne protéger que la modification de l'état. Utilisez des verrous en lecture-écriture lorsque plusieurs lecteurs n'entrent pas en conflit. Profilez avant d'optimiser, l'exactitude vient en premier.

Puis-je utiliser un stockage local au lieu de verrous ?

Oui, lorsque chaque thread a besoin de sa propre copie des données. Le stockage local aux threads élimine la surcharge de synchronisation en donnant à chaque thread un état privé. Il est utilisé pour les caches, les tampons ou les accumulateurs propres à chaque thread, qui seront fusionnés ultérieurement. Cependant, la synchronisation reste nécessaire lorsque les threads communiquent ou partagent les résultats finaux.

Qu'en est-il du verrouillage global de l'interprète (GIL) de Python ?

La GIL n'élimine pas le besoin de verrous. Bien qu'elle empêche l'exécution simultanée du bytecode Python, elle ne rend pas les opérations atomiques. Une simple incrémentation du compteur += 1 implique plusieurs opérations de bytecode où la GIL peut être libérée entre elles. Utilisez toujours une synchronisation appropriée pour les états partagés, même en CPython.

Comment tester les conditions de course ?

Utilisez des assainisseurs de threads et des outils de test de simultanéité spécifiques à votre langage. Écrire des tests de stress qui génèrent de nombreux threads effectuant des opérations simultanées et qui vérifient que les invariants sont maintenus. Augmentez le nombre de threads et d'itérations pour mettre en évidence les bogues liés au temps. Cependant, la réussite des tests ne prouve pas l'absence de conditions de course, c'est pourquoi la révision du code et une conception minutieuse de la synchronisation restent essentielles.

Qu'est-ce qu'une structure de données sans verrou et sans attente ?

Les structures de données sans verrous utilisent des opérations atomiques (comparaison et échange) au lieu de verrous, ce qui garantit un progrès à l'échelle du système même si les threads sont retardés. Les structures sans attente garantissent une progression par thread. Elles sont complexes à mettre en œuvre correctement mais offrent de meilleures performances en cas de forte contention. Utilisez des bibliothèques éprouvées (java.util.concurrent, bibliothèque atomique C++) plutôt que d'implémenter les vôtres.

Obtenir la sécurité gratuitement

Sécurisez votre code, votre cloud et votre environnement d'exécution dans un système central.
Trouvez et corrigez rapidement et automatiquement les vulnérabilités.

Aucune carte de crédit n'est requise | Scanner les résultats en 32sec.