In Go, map di default non è thread-safe. La scrittura o la modifica simultanea della map da diverse goroutine porta a condizioni di gara e panico di tipo "concurrent map writes". Per proteggere la map, di solito si utilizza sync.Mutex, sync.RWMutex o un tipo speciale sync.Map dalla libreria standard che implementa operazioni atomiche sicure.
Gli elementi possono essere rimossi dalla map tramite delete(map, key), ma durante l'iterazione sulla map (tramite range) ci sono delle sottigliezze:
Esempio di lavoro thread-safe con map:
type SafeMap struct { mu sync.RWMutex m map[string]int } func (s *SafeMap) Load(key string) (int, bool) { s.mu.RLock() v, ok := s.m[key] s.mu.RUnlock() return v, ok } func (s *SafeMap) Store(key string, value int) { s.mu.Lock() s.m[key] = value s.mu.Unlock() }
Domanda: È possibile rimuovere in sicurezza gli elementi dalla map durante l'iterazione range sulla stessa map?
Risposta: Sì, solo se si rimuove la chiave già emessa dall'iterazione range. Ma non si può modificare la map da altre goroutine in parallelo: questo porterà a una condizione di gara.
m := map[string]int{"a": 1, "b": 2, "c": 3} for k := range m { delete(m, k) // sicuro! (se fatto solo da questa goroutine) }
Storia
Uno dei servizi di monitoraggio teneva statistiche su una map e si aggiornava contemporaneamente da più goroutine (contatori delle metriche). Nel momento di picco si sono verificati panico di "concurrent map writes", il servizio si è bloccato, i dati sono andati persi. Soluzione: aggiungere mutex o utilizzare sync.Map invece della map normale.
Storia
Durante la migrazione dei dati, qualcuno ha deciso di accelerare la pulizia di una grande map con goroutine parallele, ognuna delle quali rimuoveva la propria parte di chiavi tramite range. Risultato: gare di dati costanti e crash imprevedibili. Dopo la migrazione è stato necessario tornare alla pulizia sequenziale o bloccare l'accesso durante il range.
Storia
Nel ciclo range sulla map, lo sviluppatore aggiungeva nuovi dati nella stessa map (ad esempio, formava un elenco di nodi adiacenti per un grafo). Si è scoperto che le nuove chiavi potevano essere ignorate nella corrente iterazione range, portando a un'elaborazione incompleta del grafo. Il bug è stato scoperto solo durante i test completi di casi rari. Dopo la correzione, l'algoritmo è stato riscritto utilizzando una coda separata per le aggiunte.