Перейти к основному содержимому

Потокобезопасная мапа: sync.Map

Обычная map в Go не является потокобезопасной. Если одна горутина пишет в мапу, а другая одновременно читает или пишет, программа упадет с ошибкой fatal error: concurrent map writes (отловить это через recover невозможно, приложение просто умрет).

Самое простое решение — обернуть обычную map в структуру с мьютексом (sync.RWMutex). Но в стандартной библиотеке есть специальный тип — sync.Map.

1. Зачем нужен sync.Map, если есть RWMutex?

Проблема мьютекса в том, что при огромном количестве одновременных чтений на многоядерных процессорах возникает lock contention (борьба за блокировку). Даже RLock() (блокировка на чтение) заставляет ядра процессора синхронизировать кэши между собой, что снижает производительность.

sync.Map спроектирована так, чтобы читать данные без блокировок вообще (lock-free), используя атомарные операции (atomic).

2. Как sync.Map работает под капотом?

Внутри sync.Map использует не одну, а две обычные мапы:

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

Алгоритм работы:

  • Чтение (Load): Сначала Go ищет ключ в read мапе (без блокировок). Если ключа нет, но он может быть в dirty, берется блокировка, и ключ ищется в dirty.
  • Запись (Store): Если ключ уже есть в read, значение обновляется атомарно. Если это новый ключ — берется блокировка и он пишется в dirty.
  • Промоушен (Promotion): Если чтений из dirty мапы становится слишком много (счетчик промахов misses превышает порог), dirty мапа атомарно "повышается" и становится новой read мапой.

3. Когда использовать sync.Map?

У sync.Map есть только два идеальных юзкейса (это прямо написано в официальной документации Go):

  1. Кэширование (Write-once, Read-many): Когда ключи добавляются один раз, а потом бесконечно читаются многими горутинами.
  2. Раздельные пространства ключей: Когда разные горутины работают с совершенно разными непересекающимися ключами в одной мапе.

Интервью Если на собеседовании спросят: "Что лучше использовать?", правильный ответ: "В 90% случаев обычная map + sync.RWMutex будет быстрее и понятнее. sync.Map стоит брать только если профилирование (pprof) показало, что мьютекс стал узким горлышком из-за огромного количества чтений."

4. Основные методы sync.Map

Вместо обычного синтаксиса m[key], здесь используются методы:

  • Store(key, value) — записать.
  • Load(key) (value, ok) — прочитать.
  • Delete(key) — удалить.
  • LoadOrStore(key, value) — атомарно возвращает значение, если оно есть. Если нет — записывает новое.
  • Range(func(key, value any) bool) — итерация по мапе (если функция вернет false, цикл прервется).