Proteggere i dati condivisi
Un sync.Mutex lascia entrare una sola goroutine alla volta nel codice tra Lock e Unlock. Metti il mutex accanto ai dati che protegge, di solito nella stessa struct:
Stampa sempre hits: 10000. Senza il lock, 100 goroutine che scrivono sulla stessa map di solito farebbero crashare il programma con fatal error: concurrent map writes. Quell'errore viene da un controllo del runtime fatto al meglio delle possibilità, non si può recuperare, e l'assenza di un crash non dimostra che il codice sia corretto.
Dettagli che contano:
- Lo zero value di
sync.Mutexè sbloccato e pronto. Nessun costruttore. - I metodi usano un receiver puntatore (
*Counter). Un receiver valore bloccherebbe una copia del mutex, che non protegge niente. defer c.mu.Unlock()subito dopoLocksignifica che ogni percorso di uscita, e anche un panic, rilascia il lock.- Ogni accesso passa dal lock, letture comprese. Una lettura senza lock mentre un'altra goroutine scrive è comunque una data race.
Tieni piccola la sezione critica
defer sblocca alla fine della funzione. Va bene per metodi brevi come quelli qui sopra. In una funzione più lunga, sblocca appena i dati condivisi non vengono più toccati, così le altre goroutine non aspettano lavoro che non ha bisogno del lock:
func (s *Store) Save(key string) error {
s.mu.Lock()
data := s.items[key] // copy what you need
s.mu.Unlock()
return writeToDisk(key, data) // slow I/O, outside the lock
}
Tenere un lock durante chiamate di rete, I/O su disco o un invio su channel è la causa più comune di un programma concorrente lento, e un invio su channel sotto lock è una causa comune di deadlock.
RWMutex per dati letti spesso
sync.RWMutex ha due modalità. RLock/RUnlock prendono un lock di lettura condiviso che molte goroutine possono tenere insieme. Lock/Unlock prendono il lock di scrittura esclusivo, che aspetta finché tutti i lettori non escono.
RWMutex conviene quando le letture dominano e ogni lettura fa un lavoro reale sotto il lock. Per sezioni critiche minuscole come una singola ricerca in una map, un Mutex normale è spesso altrettanto veloce, perché il lock di lettura ha una sua contabilità interna. Fai un benchmark prima di scegliere.
Non puoi trasformare un lock di lettura in un lock di scrittura. Chiamare Lock mentre tieni RLock nella stessa goroutine causa un deadlock. Rilascia prima il lock di lettura, poi prendi quello di scrittura e controlla di nuovo la condizione, perché nel frattempo un altro scrittore potrebbe aver cambiato i dati.
sync/atomic per valori singoli
Per un solo contatore o flag, sync/atomic è più semplice ed economico di un mutex. I wrapper tipizzati (Go 1.19) sono quelli da usare:
Gli atomic proteggono un valore alla volta. Non appena due valori devono cambiare insieme (un saldo e un numero di transazioni, una map e la sua dimensione), usa un mutex. Due operazioni atomic separate possono intrecciarsi con altre goroutine tra l'una e l'altra.
sync.Once
sync.Once esegue una funzione esattamente una volta, non importa quante goroutine la chiamino nello stesso momento. Chiunque chiami Do aspetta finché la prima chiamata non è terminata. È il modo standard per inizializzare qualcosa in modo pigro:
Ogni riga "runs once" compare esattamente una volta. Se la funzione passata a Do va in panic, Once la considera comunque eseguita e non riprova mai. sync.OnceValues fa lo stesso per le funzioni che restituiscono due valori, tipicamente un valore e un errore.
Mutex, channel o sync.Map
| Situazione | Usa |
|---|---|
| Una struct o una map che più goroutine aggiornano sul posto | sync.Mutex nella struct |
| Soprattutto letture, scritture occasionali, letture che fanno lavoro reale | sync.RWMutex |
| Un singolo contatore o flag | sync/atomic |
| Inizializzazione una tantum | sync.Once, sync.OnceValue |
| Passare dati da una goroutine a un'altra | un channel |
| Una cache con chiavi scritte una volta e lette molte volte, o goroutine che toccano chiavi disgiunte | sync.Map |
sync.Map non sostituisce in generale una map con lock. Non ha parametri di tipo, quindi i valori tornano come any, ed è più veloce solo nei due casi della tabella. Parti da un mutex e da una map normale.
Errori comuni
- Copiare un mutex. Passare per valore una struct che contiene un
sync.Mutex, o usare un receiver valore, copia il lock.go vetsegnalapasses lock by valueocopies lock value. - Prendere il lock due volte nella stessa goroutine. I mutex di Go non sono rientranti. Se
IncchiamaGeted entrambi prendono il lock,Incsi blocca per sempre. Fai prendere il lock ai metodi pubblici, e lascia che le funzioni di supporto private diano per scontato che sia già preso. - Dimenticare di sbloccare con un return anticipato. Usa
defera meno che tu non abbia un motivo per non farlo. - Prendere i lock in ordini diversi. Se una goroutine prende il lock A e poi B mentre un'altra prende B e poi A, possono aspettarsi a vicenda per sempre. Acquisisci sempre più lock nello stesso ordine.
- Esporre i dati protetti. Restituire la map interna da un metodo permette a chi chiama di leggerla e scriverla senza lock. Restituisci una copia (
maps.Clone, Go 1.21) o un singolo valore. - Proteggere solo le scritture. Le letture senza lock concorrenti con scritture sotto lock sono comunque race. Esegui i tuoi test con
go test -race.
Domande frequenti
Cos'è un mutex in Go?
Un sync.Mutex è un lock che permette a una sola goroutine alla volta di eseguire il codice tra mu.Lock() e mu.Unlock(). Lo usi per proteggere dati che più goroutine leggono e scrivono, come una map o una struct. Il suo zero value è un mutex sbloccato, pronto all'uso.
Quando dovrei usare RWMutex invece di Mutex?
Quando le letture sono molte più delle scritture e ogni lettura tiene il lock per un tempo significativo. RLock lascia entrare insieme un numero qualsiasi di lettori, mentre Lock aspetta l'accesso esclusivo. Per sezioni critiche brevi un Mutex normale è spesso altrettanto veloce o più veloce, quindi misura prima di cambiare.
Una map di Go è sicura per l'uso concorrente?
No. Le letture concorrenti vanno bene, ma una scrittura concorrente con qualsiasi altra lettura o scrittura è una data race, e il runtime di solito la rileva e crasha con fatal error: concurrent map writes (o concurrent map read and map write). Proteggi la map con un sync.Mutex o un sync.RWMutex, oppure usa sync.Map per i suoi casi d'uso specifici.
sync.Mutex è rientrante in Go?
No. Se una goroutine che tiene il lock chiama di nuovo Lock, si blocca per sempre aspettando se stessa. Organizza il codice in modo che i metodi esportati prendano il lock e chiamino funzioni di supporto non esportate che danno per scontato che sia già preso.