Regola
Assicurarsi che che l'accesso sia thread-safe l'accesso alle stato stato condiviso.
Stato stato stato a cui si accede da più thread
senza sincronizzazione causa condizioni condizioni di e errori errori di runtime.
Linguaggi supportati: Python, Java, C#Introduzione
Quando più thread accedono e modificano variabili condivise senza sincronizzazione, si verificano condizioni di competizione. Il valore finale dipende dai tempi di esecuzione imprevedibili dei thread, il che può causare il danneggiamento dei dati, calcoli errati o errori di esecuzione. Un contatore incrementato da più thread senza blocco non rifletterà gli aggiornamenti, poiché i thread leggono valori non aggiornati, li incrementano e riscrivono risultati in conflitto tra loro.
Perché è importante
Corruzione dei dati e risultati errati: le condizioni di competizione causano una corruzione silenziosa dei dati, in cui i valori diventano incoerenti o errati. I saldi dei conti possono risultare errati, le giacenze di magazzino possono essere negative oppure le statistiche aggregate possono risultare corrotte. Questi bug sono difficili da riprodurre perché dipendono dall’esatta sincronizzazione 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 sta leggendo, provocando eccezioni quali errori di puntatore nullo o indice fuori limiti. In produzione, ciò si manifesta con crash intermittenti sotto carico.
Complessità del debug: le condizioni di competizione sono notoriamente difficili da risolvere perché sono non deterministiche. Il bug potrebbe non manifestarsi nei test a thread singolo o in ambienti a basso carico. Per riprodurlo è necessario uno specifico intreccio di thread difficile da forzare, il che fa sì che i problemi compaiano e scompaiano in modo casuale.
Esempi di codice
❌ Non conforme:
classe ContoBancario:
def __init__(self):
self.saldo = 0
def deposit(self, importo):
saldo_attuale = self.saldo
# Condizione di competizione: un altro thread potrebbe modificare il saldo in questo punto
time.sleep(0.001) # Simula il tempo di elaborazione
self.balance = current + amount
def prelievo(self, importo):
se saldo.saldo >= importo:
current = self.saldo
time.sleep(0.001)
saldo = saldo_attuale - importo
return True
restituisci False
Perché è sbagliato: più thread che chiamano contemporaneamente le funzioni deposit() o withdraw() creano condizioni di competizione. Due thread che depositano ciascuno 100 $ potrebbero entrambi leggere un saldo pari a 0 $, quindi entrambi scrivere 100 $, con il risultato che il saldo finale sarebbe di 100 $ anziché 200 $.
✅ Conforme:
import threading
classe ContoBancario:
def __init__(self):
self.__balance = 0
self.__lock = threading.Lock()
@property
def balance(self):
con self.__lock:
restituisci self.__balance
def deposit(self, importo):
con self.__lock:
current = self.__balance
time.sleep(0.001)
self.__balance = current + amount
def withdraw(self, importo):
con self.__lock:
se self.__balance >= importo:
current = self.__balance
time.sleep(0.001)
self.__balance = current - amount
return True
return False
Perché è importante: Il threading.Lock() garantisce che solo un thread alla volta possa accedere a balance. Quando un thread detiene il blocco, gli altri attendono, impedendo modifiche simultanee. Privato __saldo__ con readonly @proprietà impedisce che codice esterno eluda la protezione tramite blocco.
Conclusione
Proteggere tutti gli stati mutabili condivisi utilizzando primitive di sincronizzazione appropriate, quali blocchi (lock), semafori o operazioni atomiche. Quando possibile, privilegiare strutture dati immutabili o l'archiviazione locale del thread. Quando la sincronizzazione è necessaria, ridurre al minimo le sezioni critiche per diminuire il contenzioso e migliorare le prestazioni.

