Praca z kolekcjami konkurencyjnymi w Go stała się istotnym tematem z powodu rosnących wymagań dotyczących aplikacji wielowątkowych. Zwykłe mapy w Go nie są bezpieczne w użyciu wątkowym i mogą prowadzić do wyścigów danych (data race). Pojawienie się sync.Map dostarczyło standardowe rozwiązanie dla bezpiecznego wspólnego dostępu do kolekcji bez zewnętrznej synchronizacji.
Historia pytania:
Przed pojawieniem się sync.Map programiści musieli używać zwykłych map z zewnętrznym Mutex lub RWMutex, aby zorganizować bezpieczny dostęp z kilku goroutines. Zwiększało to ilość kodu i prawdopodobieństwo błędów synchronizacji. W Go 1.9 wprowadzono sync.Map, aby uprościć pracę z kolekcjami konkurencyjnymi.
Problem:
Zwykła mapa nie jest bezpieczna w użyciu wątkowym. Jeśli kilka goroutines odczytuje i zapisuje do mapy bez synchronizacji, prowadzi to do paniki lub nieoczekiwanych wyników. Mutex jest skomplikowany w poprawnym użyciu i może prowadzić do blokad i pogorszenia wydajności. Występuje również problem z "double check" i pracą z trudnymi do zmierzenia synchronizacjami.
Rozwiązanie:
sync.Map to specjalna struktura w standardowej bibliotece, oferująca bezpieczne w użyciu wątkowym metody Load, Store, LoadOrStore, Delete, Range. Realizuje strategię lock-free (częściowo), zoptymalizowaną dla scenariuszy z częstym odczytem i rzadkim zapisem.
Przykład kodu:
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") }
Kluczowe cechy:
Czy można zastąpić wszystkie mapy sync.Map w programach wielowątkowych?
Nie, sync.Map nie jest uniwersalnym zamiennikiem zwykłej mapy. Dobrze nadaje się do tych struktur danych, gdzie dominują konkurencyjne, niezależne odczyty, ale przy intensywnym zapisie (częste modyfikacje) lub dla małych kolekcji zwykła mapa + Mutex są szybsze i bardziej efektywne.
Co się stanie, jeśli zwykłą mapę używać tylko do odczytu w kilku goroutinach?
Jeśli mapa jest w pełni zainicjowana i nie zmienia się po uruchomieniu wszystkich goroutines, równoległy odczyt jest dozwolony i bezpieczny. Ale jakiekolwiek usunięcie lub zmiana danych prowadzi do nieprzewidywalnego zachowania, paniki lub uszkodzonej mapy.
Jakie typy danych można używać jako klucz dla sync.Map?
Zasady są takie same, jak dla zwykłej mapy: tylko typy porównywalne (comparable types). Jednak sync.Map przyjmuje klucz dowolnego typu interfejsu{}, co może stworzyć ryzyko obiektów z różną semantyką, które nie mogą być porównywane między sobą lub w których występują błędy runtime.
Przykład kodu:
var m sync.Map m.Store([]int{1,2}, "value") // panic: runtime error: hash of unhashable type []int
Programista użył sync.Map do przechowywania ustawień aplikacji, które rzadko się zmieniają, ale często są odczytywane. Jednak później zaczęto masowo zapisywać dane o sesjach użytkowników, co doprowadziło do niespodziewanego wzrostu obciążenia GC i pogorszenia wydajności.
Zalety:
Wady:
Zespół wdrożył sync.Map do przechowywania cache'u często żądanych wyników obliczeń w serwisie o dużym obciążeniu. Liczba „odczytów” przewyższa „zapisy” setki razy. Wszystko działa stabilnie i efektywnie, kod stał się krótszy i prostszy w utrzymaniu.
Zalety:
Wady: