Ответ
sync.Map
— это встроенный в Go тип данных, представляющий собой потокобезопасную map. Он оптимизирован для специфичных сценариев использования и не является универсальной заменой для map
с мьютексом.
Как устроен sync.Map
Ключевая особенность sync.Map
— это наличие двух внутренних "карт" для минимизации блокировок при чтении:
read
(read-only map): Основная карта для чтения. Доступ к ней осуществляется атомарно, без использования мьютекса. Она содержит стабильный набор ключ-значение, который уже был "зафиксирован".dirty
(read-write map): Карта для записи. Все новые ключи, обновления и удаления сначала попадают сюда. Доступ кdirty
защищен мьютексом. Она может содержать данные, которых еще нет вread
.
Как работают операции
Чтение (
Load
):- Быстрый путь: Сначала ключ ищется в
read
карте (атомарно, без блокировки). Если найден — значение возвращается. - Медленный путь: Если в
read
ключ не найден, ставится блокировка, и поиск происходит вdirty
карте. Если ключ найден там, он возвращается.
- Быстрый путь: Сначала ключ ищется в
Запись (
Store
):- Всегда требует блокировки мьютекса.
- Запись происходит в
dirty
карту. - Если
dirty
карта становится слишком большой (содержит много новых ключей), ее содержимое "продвигается" (promote) вread
карту, делаяdirty
пустой. Стараяread
карта утилизируется сборщиком мусора.
Удаление (
Delete
):- Удаление также происходит через
dirty
карту под блокировкой. Вместо реального удаления изread
карты, значение в ней помечается как удаленное (expunged), чтобы избежать дорогостоящей перестройки.
- Удаление также происходит через
Когда использовать sync.Map
sync.Map
наиболее эффективен в следующих случаях:
- Кэширование (write-once, read-many): Когда ключи записываются один раз, а затем многократно читаются разными горутинами. Например, кэш метаданных или конфигурации.
- Раздельный доступ: Когда разные горутины в основном работают с разными, непересекающимися наборами ключей. Это минимизирует конкуренцию за
dirty
карту.
Когда НЕ использовать sync.Map
(и что использовать вместо)
В большинстве других случаев традиционный подход map + sync.RWMutex
будет проще и производительнее:
- Частые записи: Если в вашем приложении много операций записи или обновления, постоянные блокировки для доступа к
dirty
карте сведут на нет все преимуществаsync.Map
. - Стабильный набор ключей: Если вы заранее знаете все ключи и они не меняются, обычная
map
будет самым быстрым решением (данные можно безопасно читать из разных горутин после инициализации). - Необходимость итерации:
sync.Map
не предоставляет способа получить количество элементов или эффективно итерироваться по всем ключам, как это делает обычнаяmap
сrange
. МетодRange
не гарантирует обход всех элементов, если в процессе происходят записи.
Пример использования:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Запись данных (например, при инициализации кэша)
m.Store("user:1", "Alice")
m.Store("user:2", "Bob")
// Чтение данных из разных горутин
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
val, ok := m.Load("user:1")
if ok {
fmt.Println("Goroutine 1:", val)
}
}()
go func() {
defer wg.Done()
val, ok := m.Load("user:2")
if ok {
fmt.Println("Goroutine 2:", val)
}
}()
wg.Wait()
// Итерация по карте
m.Range(func(key, value interface{}) bool {
fmt.Printf("Iterating: %s -> %sn", key, value)
return true // возвращаем true для продолжения итерации
})
}