Ochrona współdzielonych danych
sync.Mutex wpuszcza do kodu między Lock a Unlock tylko jedną gorutynę naraz. Umieść mutex obok danych, które chroni, zwykle w tej samej strukturze:
To zawsze wypisuje hits: 10000. Bez blokady 100 gorutyn zapisujących tę samą mapę zwykle wywróciłoby program z fatal error: concurrent map writes. Ten błąd pochodzi z kontroli, którą runtime wykonuje na zasadzie najlepszych starań, nie da się go przechwycić, a brak awarii nie dowodzi, że kod jest poprawny.
Szczegóły, które mają znaczenie:
- Wartość zerowa
sync.Mutexjest odblokowana i gotowa. Żadnego konstruktora. - Metody mają odbiorcę wskaźnikowego (
*Counter). Odbiorca wartościowy blokowałby kopię mutexu, co niczego nie chroni. defer c.mu.Unlock()zaraz poLockoznacza, że każda ścieżka powrotu, a także panika, zwalnia blokadę.- Każdy dostęp przechodzi przez blokadę, łącznie z odczytami. Odczyt bez blokady, gdy inna gorutyna zapisuje, to nadal wyścig danych.
Sekcja krytyczna ma być mała
defer zwalnia blokadę na końcu funkcji. To dobre rozwiązanie dla krótkich metod, takich jak powyższe. W dłuższej funkcji zwolnij blokadę, gdy tylko przestajesz dotykać współdzielonych danych, żeby inne gorutyny nie czekały na pracę, która blokady nie potrzebuje:
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
}
Trzymanie blokady podczas wywołań sieciowych, operacji dyskowych albo wysyłki do kanału to najczęstsza przyczyna powolnego programu współbieżnego, a wysyłka do kanału pod blokadą to częsta przyczyna zakleszczenia.
RWMutex dla danych głównie czytanych
sync.RWMutex ma dwa tryby. RLock/RUnlock biorą współdzieloną blokadę odczytu, którą może naraz trzymać wiele gorutyn. Lock/Unlock biorą wyłączną blokadę zapisu, która czeka, aż wszyscy czytelnicy wyjdą.
RWMutex się opłaca, gdy dominują odczyty, a każdy odczyt wykonuje pod blokadą prawdziwą pracę. Przy maleńkich sekcjach krytycznych, np. pojedynczym odczycie z mapy, zwykły Mutex bywa równie szybki, bo blokada odczytu ma własną księgowość. Zrób benchmark, zanim wybierzesz.
Nie da się podnieść blokady odczytu do blokady zapisu. Wywołanie Lock przy trzymanym RLock w tej samej gorutynie prowadzi do zakleszczenia. Najpierw zwolnij blokadę odczytu, potem weź blokadę zapisu i ponownie sprawdź warunek, bo w międzyczasie inny zapisujący mógł zmienić dane.
sync/atomic dla pojedynczych wartości
Dla jednego licznika lub flagi sync/atomic jest prostsze i tańsze niż mutex. Warto używać typowanych opakowań (Go 1.19):
Operacje atomowe chronią jedną wartość naraz. Gdy tylko dwie wartości muszą zmieniać się razem (saldo i liczba transakcji, mapa i jej rozmiar), użyj mutexu. Między dwiema osobnymi operacjami atomowymi mogą się wcisnąć inne gorutyny.
sync.Once
sync.Once wykonuje funkcję dokładnie raz, niezależnie od tego, ile gorutyn wywoła ją jednocześnie. Każdy, kto wywoła Do, czeka, aż pierwsze wywołanie się zakończy. To standardowy sposób na leniwą inicjalizację:
Każda linia „runs once” pojawia się dokładnie raz. Jeśli funkcja przekazana do Do wywoła panikę, Once i tak uznaje ją za wykonaną i nigdy nie ponawia. sync.OnceValues robi to samo dla funkcji zwracających dwie wartości, zwykle wartość i błąd.
Mutex, kanał czy sync.Map
| Sytuacja | Użyj |
|---|---|
| Struktura lub mapa, którą kilka gorutyn aktualizuje w miejscu | sync.Mutex w strukturze |
| Głównie odczyty, sporadyczne zapisy, odczyty wykonują prawdziwą pracę | sync.RWMutex |
| Pojedynczy licznik lub flaga | sync/atomic |
| Jednorazowa inicjalizacja | sync.Once, sync.OnceValue |
| Przekazywanie danych z jednej gorutyny do drugiej | kanał |
| Cache, którego klucze są zapisywane raz i czytane wiele razy, albo gorutyny pracujące na rozłącznych kluczach | sync.Map |
sync.Map nie jest ogólnym zamiennikiem mapy z blokadą. Nie ma parametrów typu, więc wartości wracają jako any, i jest szybsza tylko w dwóch przypadkach z tabeli. Zacznij od mutexu i zwykłej mapy.
Częste błędy
- Kopiowanie mutexu. Przekazanie przez wartość struktury zawierającej
sync.Mutexalbo użycie odbiorcy wartościowego kopiuje blokadę.go vetzgłaszapasses lock by valuealbocopies lock value. - Dwukrotne blokowanie w jednej gorutynie. Mutexy w Go nie są reentrant. Jeśli
IncwywołujeGeti obie biorą blokadę,Incblokuje się na zawsze. Niech metody publiczne blokują, a prywatne funkcje pomocnicze zakładają, że blokada jest wzięta. - Brak zwolnienia blokady przy wczesnym powrocie. Używaj
defer, chyba że masz powód, żeby tego nie robić. - Blokowanie w różnej kolejności. Jeśli jedna gorutyna bierze blokadę A, a potem B, a druga B, a potem A, obie mogą czekać na siebie w nieskończoność. Zawsze zdobywaj kilka blokad w tej samej kolejności.
- Wystawianie chronionych danych na zewnątrz. Zwrócenie wewnętrznej mapy z metody pozwala wywołującym czytać ją i zapisywać bez blokady. Zwracaj kopię (
maps.Clone, Go 1.21) albo pojedynczą wartość. - Ochrona tylko zapisów. Odczyty bez blokady równoległe z zapisami pod blokadą to nadal wyścigi. Uruchamiaj testy przez
go test -race.
Najczęściej zadawane pytania
Czym jest mutex w Go?
sync.Mutex to blokada, która pozwala tylko jednej gorutynie naraz wykonywać kod między mu.Lock() a mu.Unlock(). Używasz jej do ochrony danych, które czyta i zapisuje kilka gorutyn, np. mapy albo struktury. Jej wartość zerowa to odblokowany mutex gotowy do użycia.
Kiedy używać RWMutex zamiast Mutex?
Gdy odczytów jest znacznie więcej niż zapisów, a każdy odczyt trzyma blokadę przez odczuwalny czas. RLock wpuszcza naraz dowolną liczbę czytelników, a Lock czeka na wyłączny dostęp. Przy krótkich sekcjach krytycznych zwykły Mutex bywa równie szybki albo szybszy, więc zmierz, zanim zmienisz.
Czy mapa w Go jest bezpieczna przy współbieżnym użyciu?
Nie. Współbieżne odczyty są w porządku, ale zapis równoległy z jakimkolwiek innym odczytem lub zapisem to wyścig danych, a runtime zwykle go wykrywa i wywraca program z fatal error: concurrent map writes (albo concurrent map read and map write). Chroń mapę przez sync.Mutex albo sync.RWMutex lub użyj sync.Map w jej konkretnych zastosowaniach.
Czy sync.Mutex w Go jest reentrant?
Nie. Jeśli gorutyna, która trzyma blokadę, ponownie wywoła Lock, zablokuje się na zawsze, czekając na samą siebie. Ułóż kod tak, żeby metody eksportowane brały blokadę i wywoływały nieeksportowane funkcje pomocnicze, które zakładają, że blokada jest już wzięta.