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

Как сделать map потокобезопасной?

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

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

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

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

Потокобезопасность map в Go

Map в Go не является потокобезопасной по своей природе — одновременные операции чтения и записи из нескольких горутин приведут к состоянию гонки (race condition) и могут вызвать панику. Существует несколько подходов для обеспечения потокобезопасности.

1. Использование мьютексов (Самый распространённый способ)

Наиболее простой и эффективный подход — обернуть map в структуру с мьютексом для синхронизации доступа.

package main

import "sync"

type SafeMap struct {
    mu sync.RWMutex
    data map[string]interface{}
}

func NewSafeMap() *SafeMap {
    return &SafeMap{
        data: make(map[string]interface{}),
    }
}

func (sm *SafeMap) Set(key string, value interface{}) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.data[key] = value
}

func (sm *SafeMap) Get(key string) (interface{}, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    value, ok := sm.data[key]
    return value, ok
}

func (sm *SafeMap) Delete(key string) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    delete(sm.data, key)
}

func (sm *SafeMap) Len() int {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    return len(sm.data)
}

Преимущества подхода:

  • Полный контроль над синхронизацией
  • RWMutex позволяет множественное параллельное чтение
  • Простота реализации и понимания

Недостатки:

  • Мьютекс становится узким местом при высокой конкуренции
  • Необходимость вручную управлять блокировками

2. Синхронизированная map из sync.Map

Go 1.9+ предоставляет sync.Map, оптимизированную для двух сценариев:

  1. Когда ключ записывается один раз, а читается много раз
  2. Когда разные горутины работают с непересекающимися наборами ключей
package main

import "sync"

func main() {
    var sm sync.Map
    
    // Запись
    sm.Store("key1", "value1")
    sm.Store("key2", 42)
    
    // Чтение
    value, ok := sm.Load("key1")
    if ok {
        strVal := value.(string)
        println(strVal)
    }
    
    // Удаление
    sm.Delete("key2")
    
    // Итерация
    sm.Range(func(key, value interface{}) bool {
        println(key, value)
        return true // продолжить итерацию
    })
}

Преимущества sync.Map:

  • Оптимизирована для определённых паттернов доступа
  • Не требует явных блокировок
  • Эффективнее при многих параллельных чтениях

Недостатки:

  • Меньшая производительность для общего случая
  • Нет типизации (используется interface{})
  • Нет прямого доступа к len()

3. Шардирование (Sharding)

Для высоконагруженных систем применяется техника шардирования — разделение одной map на множество независимых map с отдельными мьютексами.

package main

import "sync"

type ShardedMap struct {
    shards []*Shard
    shardCount int
}

type Shard struct {
    mu sync.RWMutex
    data map[string]interface{}
}

func NewShardedMap(shardCount int) *ShardedMap {
    shards := make([]*Shard, shardCount)
    for i := 0; i < shardCount; i++ {
        shards[i] = &Shard{
            data: make(map[string]interface{}),
        }
    }
    return &ShardedMap{
        shards: shards,
        shardCount: shardCount,
    }
}

func (sm *ShardedMap) getShard(key string) *Shard {
    // Простая хэш-функция для определения шарда
    hash := fnv32(key)
    return sm.shards[hash%uint32(sm.shardCount)]
}

func fnv32(key string) uint32 {
    hash := uint32(2166136261)
    for i := 0; i < len(key); i++ {
        hash *= 16777619
        hash ^= uint32(key[i])
    }
    return hash
}

func (sm *ShardedMap) Set(key string, value interface{}) {
    shard := sm.getShard(key)
    shard.mu.Lock()
    defer shard.mu.Unlock()
    shard.data[key] = value
}

Преимущества шардирования:

  • Высокая параллельность (разные ключи блокируют разные мьютексы)
  • Хорошо масштабируется на многоядерных системах

Недостатки:

  • Сложность реализации
  • Невозможность атомарных операций над всей map

4. Каналы для сериализации доступа (Actor Model)

Альтернативный подход — использование канала для сериализации всех операций с map.

package main

type command struct {
    action string // "get", "set", "delete"
    key string
    value interface{}
    result chan interface{}
}

type ChannelMap struct {
    commands chan command
    data map[string]interface{}
}

func NewChannelMap() *ChannelMap {
    cm := &ChannelMap{
        commands: make(chan command),
        data: make(map[string]interface{}),
    }
    go cm.run()
    return cm
}

func (cm *ChannelMap) run() {
    for cmd := range cm.commands {
        switch cmd.action {
        case "set":
            cm.data[cmd.key] = cmd.value
            cmd.result <- nil
        case "get":
            cmd.result <- cm.data[cmd.key]
        case "delete":
            delete(cm.data, cmd.key)
            cmd.result <- nil
        }
    }
}

Критерии выбора подхода

  • sync.Mutex/RWMutex: Подходит для большинства случаев, прост в использовании
  • sync.Map: Используйте когда:
    • У вас много горутин, работающих с непересекающимися ключами
    • Ключи записываются один раз и часто читаются
  • Шардирование: Для высоконагруженных систем с равномерным распределением ключей
  • Каналы: Когда нужна строгая сериализация или интеграция с акторной моделью

Важные замечания

  1. Не используйте map с конкурентным доступом без синхронизации — это приведёт к неопределённому поведению
  2. При итерации по map всегда используйте синхронизацию, даже если только читаете
  3. Для сложных атомарных операций используйте Compare-and-Swap (CAS) или версионирование

Выбор конкретного подхода зависит от паттернов доступа к данным, требований к производительности и сложности реализации, которую вы готовы поддерживать.

Как сделать map потокобезопасной? | PrepBro