Как работает sync.Map?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает sync.Map в Go
sync.Map — это специализированная структура данных в стандартной библиотеке Go, предназначенная для безопасного использования карты (map) в конкурентных сценариях (concurrent goroutines). Она появилась как ответ на проблему стандартного map[K]V, который не является безопасным для одновременных операций чтения и записи из нескольких горутин без дополнительной синхронизации.
Основная проблема стандартного map
// НЕбезопасный код - может вызвать панику (panic) или data corruption
m := make(map[string]int)
go func() {
m["key"] = 1 // Письмо
}()
go func() {
_ = m["key"] // Чтение
}()
// Требуется внешняя синхронизация (mutex, RWMutex)
Ключевые принципы работы sync.Map
sync.Map использует несколько нетривиальных техник для минимизации блокировок и повышения производительности в высококонкурентных сценариях:
1. Двойное представление данных: read и dirty
Внутренняя структура содержит две карты:
read map(atomic.Value): Основная карта для операций чтения. Она доступна атомарно (без блокировок) для чтения и обновления существующих ключей. Это "cache-friendly" представление.dirty map: Вспомогательная карта для операций записи новых ключей и полного обновления. Доступ к ней требует блокировки мьютекса (sync.Mutex).
// Внутренняя структура (схематично)
type Map struct {
mu Mutex
read atomic.Value // хранит *readOnly
dirty map[interface{}]*entry
misses int
}
2. Стратегия "promotion" (перемещение из dirty в read)
Когда операция чтения не может найти ключ в read (промах — miss), она под мьютексом проверяет dirty. Если количество промахов (misses) достигает размера dirty, происходит "promotion": dirty целиком становится новым read, а новый dirty создаётся как nil. Это позволяет "горячим" ключам мигрировать в быстрое read представление.
3. Удаление через атомарные указатели и маркировку
Каждое значение хранится как *entry, который содержит атомарный указатель p на интерфейс (interface{}). Удаление реализовано не физическим удалением из карты, а атомарной установкой p = nil. Позже, при операции записи, nil-entries физически удаляются из dirty.
// Операция удаления (Delete)
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(*readOnly)
e, ok := read.m[key]
if !ok && read.amended {
// Ключ может быть в dirty - блокировка
m.mu.Lock()
// ... установка e.p = nil атомарно
m.mu.Unlock()
}
}
Когда использовать sync.Map?
- Высокая конкурентность: Много горутин одновременно работают с картой.
- Disjoint key access patterns: Ключи, записанные одной горутиной, преимущественно читаются той же или другими горутинами, но не часто переписываются всеми.
- Ключи стабильны: Большинство ключей записываются один раз и много читаются.
- НЕ для замены всех map: В сценариях с низкой конкурентностью или частыми записями в общие ключи стандартный
map + sync.RWMutexможет быть эффективнее.
Пример использования
import "sync"
func main() {
var sm sync.Map
// Параллельные записи
for i := 0; i < 10; i++ {
go func(id int) {
sm.Store("key"+id, id*100)
}(i)
}
// Чтение
val, ok := sm.Load("key5")
if ok {
fmt.Println("Loaded:", val)
}
// Удаление
sm.Delete("key1")
// Итерация (использует внутренний read snapshot)
sm.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true // продолжать итерацию
})
}
Основные методы API
Store(key, value): Запись или обновление.Load(key): Чтение, возвращает (value, ok).LoadOrStore(key, value): Чтение, если ключ есть, иначе запись.Delete(key): Удаление.Range(func(key, value) bool): Итерация по всем элементам.
sync.Map — это сложная, оптимизированная структура, которая не является универсальной заменой для мьютексов. Она эффективна в специфических сценариях "read-mostly" с высокой конкурентностью, где её внутренняя оптимизация минимизирует блокировки. В остальных случаях ручная синхронизация может дать лучшую производительность и контроль.