← Назад к вопросам

Как работает sync.Map?

2.0 Middle🔥 251 комментариев
#Конкурентность и горутины#Производительность и оптимизация

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работает 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" с высокой конкурентностью, где её внутренняя оптимизация минимизирует блокировки. В остальных случаях ручная синхронизация может дать лучшую производительность и контроль.