Расскажите о `sync.Map`: как он устроен и в каких случаях его следует использовать?

Ответ

sync.Map — это встроенный в Go тип данных, представляющий собой потокобезопасную map. Он оптимизирован для специфичных сценариев использования и не является универсальной заменой для map с мьютексом.

Как устроен sync.Map

Ключевая особенность sync.Map — это наличие двух внутренних "карт" для минимизации блокировок при чтении:


  1. read (read-only map): Основная карта для чтения. Доступ к ней осуществляется атомарно, без использования мьютекса. Она содержит стабильный набор ключ-значение, который уже был "зафиксирован".



  2. dirty (read-write map): Карта для записи. Все новые ключи, обновления и удаления сначала попадают сюда. Доступ к dirty защищен мьютексом. Она может содержать данные, которых еще нет в read.


Как работают операции

  • Чтение (Load):

    1. Быстрый путь: Сначала ключ ищется в read карте (атомарно, без блокировки). Если найден — значение возвращается.
    2. Медленный путь: Если в read ключ не найден, ставится блокировка, и поиск происходит в dirty карте. Если ключ найден там, он возвращается.
  • Запись (Store):

    1. Всегда требует блокировки мьютекса.
    2. Запись происходит в dirty карту.
    3. Если dirty карта становится слишком большой (содержит много новых ключей), ее содержимое "продвигается" (promote) в read карту, делая dirty пустой. Старая read карта утилизируется сборщиком мусора.
  • Удаление (Delete):

    1. Удаление также происходит через 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 для продолжения итерации
    })
}