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

Что такое sync.Map и когда его использовать вместо обычной map с мьютексом?

2.0 Middle🔥 81 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Что такое sync.Map?

sync.Map — это тип данных из стандартной библиотеки Go (пакет sync), предоставляющий потокобезопасную (thread-safe) ассоциативную карту (map), предназначенную для использования в конкурентных сценариях. В отличие от обычного map, который требует внешней синхронизации (например, с помощью sync.RWMutex), sync.Map инкапсулирует механизмы синхронизации внутри себя, предлагая атомарные операции для работы с данными.

Ключевые особенности и отличия от map + sync.RWMutex

1. Внутренняя структура и оптимизации

sync.Map спроектирована для двух основных паттернов использования:

  • Чтение преобладает над записью (read-heavy workloads).
  • Записи в разные ключи не конкурируют (keys are write-once или редко перезаписываются).

Она использует двухуровневое хранение данных:

  • read (atomic.Value): хранит "горячие" данные для быстрого чтения без блокировок.
  • dirty (map[interface{}]*entry): хранит полный набор данных, требует мьютекса для доступа.
  • misses (счётчик): отслеживает промахи кэша для переключения read и dirty.
package main

import (
    "fmt"
    "sync"
)

func main() {
    var sm sync.Map
    
    // Безопасное хранение
    sm.Store("key1", 42)
    sm.Store("key2", "hello")
    
    // Безопасное чтение
    if val, ok := sm.Load("key1"); ok {
        fmt.Println("key1:", val) // key1: 42
    }
    
    // Безопасное удаление
    sm.Delete("key2")
    
    // Итерация (может не отражать недавние изменения)
    sm.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true // продолжить итерацию
    })
}

2. Сравнение производительности

Аспектsync.Mapmap + sync.RWMutex
Чтение при низкой конкуренцииНемного медленнее из-за атомарных операцийБыстрее (простой доступ к map)
Чтение при высокой конкуренцииЗначительно быстрее (чтение без блокировок)Замедляется (блокировки читателей)
ЗаписьМедленнее (сложная внутренняя логика)Быстрее (простая блокировка)
ПамятьВыше (две копии данных + метаданные)Ниже (одна map)

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

Рекомендуемые сценарии

  1. Высокая конкуренция на чтение, редкие записи
    • Кэши, конфигурации, справочники.
    • Данные, которые инициализируются один раз (при старте), затем многократно читаются.
// Кэш метрик, обновляемый раз в минуту
var metricCache sync.Map

func GetMetric(key string) (float64, bool) {
    val, ok := metricCache.Load(key)
    if !ok {
        return 0, false
    }
    return val.(float64), true
}

func UpdateMetrics(newData map[string]float64) {
    for k, v := range newData {
        metricCache.Store(k, v) // редкие записи
    }
}
  1. Ключи преимущественно записываются один раз

    • Регистрация сессий пользователей.
    • Кэширование результатов вычислений (memoization).
  2. Каждая goroutine работает со своим набором ключей

    • Шардированные данные, где записи в разные ключи редко конфликтуют.

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

  1. Частые записи в те же ключи
    • Счётчики, накапливаемые статистики.
    • Используйте map + sync.RWMutex или атомарные типы.
// ПЛОХО для счётчика:
var badCounter sync.Map

// ХОРОШО для счётчика:
type Counter struct {
    mu sync.RWMutex
    m  map[string]int
}

// ИЛИ используйте atomic:
var goodCounter int64
atomic.AddInt64(&goodCounter, 1)
  1. Требуется предсказуемая производительность

    • Внутренняя сложность sync.Map может давать всплески задержек при перестроении данных.
  2. Необходимы транзакционные операции

    • sync.Map не поддерживает атомарные операции с несколькими ключами.

Практические рекомендации

Правило выбора:

  1. Начните с map + sync.RWMutex — это проще для понимания и отладки.
  2. Переходите на sync.Map только при доказанных проблемах с производительностью чтения.
  3. Профилируйте оба варианта под реальную нагрузку.

Важные ограничения sync.Map:

  • Нет типизации (использует interface{} в Go <1.18, any в новых версиях).
  • Range может не отражать последние изменения (снимок на момент вызова).
  • Нет методов для получения размера или всех ключей (требуется итерация).
// Пример проблемы с Range
var m sync.Map
m.Store("a", 1)

go func() {
    m.Range(func(k, v interface{}) bool {
        // Может не увидеть "b"
        fmt.Println(k, v)
        return true
    })
}()

m.Store("b", 2) // Может быть пропущено в Range выше
time.Sleep(time.Millisecond)

Вывод

sync.Map — это специализированный инструмент для конкретных конкурентных сценариев, а не замена обычной map. Используйте его когда:

  • Чтение доминирует над записью
  • Ключи редко перезаписываются
  • Высокая конкуренция между горутинами

Для большинства случаев map с sync.RWMutex остаётся предпочтительным выбором из-за простоты, предсказуемости и лучшей производительности при смешанных нагрузках. Всегда проверяйте выбор через бенчмарки под вашу конкретную рабочую нагрузку.

Что такое sync.Map и когда его использовать вместо обычной map с мьютексом? | PrepBro