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 et modifient des variables partagées sans synchronisation, des race conditions se produisent. La valeur finale dépend du timing imprévisible de l'exécution des threads, entraînant une corruption des données, des calculs incorrects ou des erreurs runtime. Un compteur incrémenté par plusieurs threads sans verrouillage manquera des mises à jour car les threads liront des valeurs obsolètes, les incrémenteront et écriront des résultats conflictuels.
Pourquoi c'est important
Corruption de données et résultats incorrects : Les conditions de concurrence entraînent une corruption silencieuse des données où les valeurs deviennent incohérentes ou incorrectes. Les soldes de compte peuvent être erronés, les décomptes d'inventaire peuvent être négatifs, ou les statistiques agrégées peuvent être corrompues. Ces bugs sont difficiles à reproduire car ils dépendent du timing exact des threads.
Instabilité du système : L'accès non synchronisé à un état partagé peut faire planter les applications. Un thread peut modifier une structure de données pendant qu'un autre la lit, provoquant des exceptions comme des erreurs de pointeur nul ou des dépassements d'index. En production, ces problèmes se manifestent par des plantages intermittents sous charge.
Complexité du débogage : Les conditions de concurrence (race conditions) sont notoirement difficiles à déboguer car elles sont non déterministes. Le bug peut ne pas apparaître dans les tests mono-thread ou les environnements à faible charge. La reproduction nécessite un entrelacement de threads spécifique difficile à forcer, ce qui fait que les problèmes apparaissent et disparaissent aléatoirement.
Exemples de code
❌ Non conforme :
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
current = self.balance
# Condition de concurrence : 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 c'est incorrect : Plusieurs threads appelant simultanément deposit() ou withdraw() créent des conditions de concurrence. Deux threads déposant 100 $ chacun pourraient tous deux lire le solde comme 0 $, puis tous deux écrire 100 $, ce qui entraînerait un solde final de 100 $ au lieu de 200 $.
✅ Conforme :
import threading
class BankAccount:
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:
if self.__balance >= amount:
current = self.__balance
time.sleep(0.001)
self.__balance = current - amount
return True
return False
Pourquoi c'est important : Le threading.Lock() garantit qu'un seul thread accède au solde à la fois. Lorsqu'un thread détient le verrou, les autres attendent, empêchant les modifications simultanées. Privé __balance avec readonly @property empêche le code externe de contourner la protection par verrouillage.
Conclusion
Protégez tout état mutable partagé avec des 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 aux threads lorsque cela est possible. Lorsque la synchronisation est nécessaire, minimisez les sections critiques pour réduire la contention et améliorer les performances.
.avif)
