Regola
Garantisci un accesso thread-safe allo stato condiviso.
Lo stato mutabile condiviso accessibile da thread
multipli senza sincronizzazione provoca race condition ed errori di runtime.
Linguaggi supportati: Python, Java, C#Introduzione
Quando più thread accedono a variabili condivise e le modificano senza sincronizzazione, si verificano race condition. Il valore finale dipende dai tempi di esecuzione imprevedibili dei thread, causando corruzione dei dati, calcoli errati o errori di runtime. Un contatore incrementato da più thread senza alcun meccanismo di locking perderà gli aggiornamenti nel momento in cui i thread leggono valori obsoleti, li incrementano e riscrivono risultati in conflitto.
Perché è importante
Corruzione dei dati e risultati errati: le race condition causano una corruzione silenziosa dei dati in cui i valori diventano incoerenti o errati. I saldi dei conti possono essere sbagliati, i conteggi dell'inventario possono essere negativi o le statistiche aggregate possono essere corrotte. Questi bug sono difficili da riprodurre perché dipendono dall'esatta tempistica dei thread.
Instabilità del sistema: l'accesso non sincronizzato allo stato condiviso può causare il crash delle applicazioni. Un thread potrebbe modificare una struttura dati mentre un altro la legge, causando eccezioni come errori di puntatore nullo o indici fuori dai limiti. In produzione, questi problemi si manifestano come crash intermittenti sotto carico.
Complessità di debug: le race condition sono notoriamente difficili da debuggare perché non deterministiche. Il bug potrebbe non comparire nei test a singolo thread o in ambienti a basso carico. La richiesta di riproduzione necessita di un'interfoliazione specifica dei thread che è difficile da forzare, facendo apparire e scomparire i problemi in modo casuale.
Esempi di codice
❌ Non conforme:
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
current = self.balance
# Race condition: un altro thread può modificare il saldo qui
time.sleep(0.001) # Simula il tempo di elaborazione
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
Perché è errato: più thread che chiamano deposit() o withdraw() contemporaneamente creano race condition. Due thread che depositano ciascuno 100$ potrebbero entrambi leggere il saldo come 0$, e poi entrambi scrivere 100$, risultando in un saldo finale di 100$ anziché di 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
Perché questo è importante: Il parametro threading.Lock() garantisce che un solo thread alla volta acceda al saldo. Quando un thread detiene il lock, gli altri attendono, impedendo modifiche simultanee. Privato __balance con sola lettura @property impedisce al codice esterno di aggirare la protezione del lock.
Conclusione
Proteggi tutto lo stato mutabile condiviso con primitive di sincronizzazione appropriate come lock, semafori o operazioni atomiche. Quando possibile, preferisci strutture dati immutabili o storage locale ai thread. Quando la sincronizzazione è necessaria, riduci al minimo le sezioni critiche per diminuire la contesa e migliorare le prestazioni.

