Lavorare con collezioni concorrenti in Go è diventato un tema importante a causa delle crescenti esigenze delle applicazioni multithread. Le mappe normali in Go non sono thread-safe e possono portare a condizioni di competizione (data race). L'introduzione di sync.Map ha fornito una soluzione standard per l'accesso sicuro condiviso alle collezioni senza sincronizzazione esterna.
Storia della questione:
Prima dell'arrivo di sync.Map, gli sviluppatori dovevano usare mappe normali con Mutex o RWMutex esterni per organizzare l'accesso sicuro da più goroutine. Questo aumentava la quantità di codice e la probabilità di errori di sincronizzazione. Nella versione 1.9 di Go è stata presentata sync.Map per semplificare il lavoro con le collezioni concorrenti.
Problema:
Una mappa normale non è thread-safe. Se più goroutine leggono e scrivono in una mappa senza sincronizzazione, ciò porta a panic o risultati imprevisti. L'uso di Mutex è complesso e può portare a deadlock e degradazione delle prestazioni. Ci sono anche complicazioni con il "double check" e il lavoro con sincronizzazione difficile da misurare.
Soluzione:
sync.Map è una struttura speciale della libreria standard che fornisce metodi thread-safe Load, Store, LoadOrStore, Delete, Range. Implementa una strategia lock-free (parzialmente), ottimizzata per scenari con letture frequenti e scritture rare.
Esempio di codice:
import ( "fmt" "sync" ) func main() { var m sync.Map m.Store("foo", 42) value, ok := m.Load("foo") fmt.Println(value, ok) // 42 true m.Delete("foo") }
Caratteristiche chiave:
È possibile sostituire tutte le mappe con sync.Map nei programmi multithread?
No, sync.Map non è una sostituzione universale per le mappe normali. Si adatta bene a quelle strutture dati dove predominano letture concorrenti e indipendenti, ma in caso di scritture intensive (modifiche frequenti) o per collezioni piccole, le mappe normali + Mutex sono più veloci ed efficienti.
Cosa succede se una mappa normale viene utilizzata solo per lettura in più goroutine?
Se la mappa è completamente inizializzata e non viene modificata dopo l'avvio di tutte le goroutine, la lettura parallela è consentita e sicura. Ma qualsiasi rimozione o modifica dei dati porterà a comportamenti imprevedibili, panico o map corrotta.
Quali tipi di dati possono essere utilizzati come chiave per sync.Map?
Le regole sono le stesse delle mappe normali: solo tipi comparabili. Tuttavia, sync.Map accetta chiavi di qualsiasi tipo interfaccia{}, il che può creare il rischio di oggetti con semantiche diverse che non possono essere confrontati o che possono portare a errori a runtime.
Esempio di codice:
var m sync.Map m.Store([]int{1,2}, "value") // panic: errore di runtime: hash di tipo non hashable []int
Uno sviluppatore ha utilizzato sync.Map per memorizzare le impostazioni dell'applicazione, che cambiano raramente, ma vengono lette frequentemente. Tuttavia, in seguito hanno iniziato a scrivere massicciamente dati sulle sessioni degli utenti, causando un'improvvisa crescita del carico del GC e degrado delle prestazioni.
Pro:
Contro:
Il team ha implementato sync.Map per memorizzare la cache dei risultati di calcolo frequentemente richiesti in un servizio ad alto carico. Il numero di “letture” supera di centinaia di volte il numero di “scritture”. Tutto funziona in modo stabile ed efficiente, il codice è diventato più breve e più facile da mantenere.
Pro:
Contro: