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

Что произойдёт если конкурентно писать в map?

1.6 Junior🔥 171 комментариев
#Основы Go

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

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

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

Короткий ответ: Неопределённое поведение и крах программы

Если вы будете конкурентно писать (или одновременно читать и писать) в одну и ту же мапу в Go без синхронизации, это приведёт к неопределённому поведению (undefined behavior). Программа может:

  • Упасть с фатальной ошибкой fatal error: concurrent map writes
  • Зависнуть (deadlock)
  • Тихо повредить данные внутри мапы
  • Выдать некорректные результаты

Почему это происходит: внутреннее устройство map в Go

Мапы в Go не являются потокобезопасными по умолчанию. Причина кроется в их внутренней реализации:

// Упрощённое представление структуры hmap (реальная реализация сложнее)
type hmap struct {
    count     int    // количество элементов
    flags     uint8
    B         uint8  // логарифм количества бакетов
    hash0     uint32 // seed для хэш-функции
    buckets   unsafe.Pointer // массив бакетов
    // ... другие поля
}

Когда происходят конкурентные записи, могут возникнуть следующие проблемы:

1. Повреждение структур данных

Два горутина могут одновременно пытаться:

  • Изменить одно поле структуры hmap
  • Рехешировать мапу (увеличивать количество бакетов)
  • Обновлять один и тот же бакет
// Пример опасного кода
func unsafeWrite() {
    m := make(map[int]string)
    
    for i := 0; i < 100; i++ {
        go func(i int) {
            m[i] = fmt.Sprintf("value%d", i) // КОНКУРЕНТНАЯ ЗАПИСЬ!
        }(i)
    }
    
    time.Sleep(time.Second)
    fmt.Println(m)
}

2. Состояние гонки (Data Race)

Даже если программа не упадёт сразу, состояние гонки может привести к тонким ошибкам:

func dataRaceExample() {
    m := map[string]int{"a": 1}
    
    // Горутина 1: читает и пишет
    go func() {
        m["a"] = m["a"] + 1 // Чтение + запись = состояние гонки
    }()
    
    // Горутина 2: тоже читает и пишет
    go func() {
        m["a"] = m["a"] * 2 // Ещё одно состояние гонки
    }()
    
    time.Sleep(time.Millisecond)
    // Результат непредсказуем: может быть 2, 3, 4 или что-то ещё
}

Как обнаружить проблему

Go имеет встроенные инструменты для обнаружения состояний гонки:

# Запуск с детектором гонок
go run -race your_program.go

# Тестирование с детектором гонок
go test -race ./...

Детектор гонок выведет предупреждение вида:

WARNING: DATA RACE
Write at 0x00c0000b6000 by goroutine 7:
  runtime.mapassign_fast64()

Правильные решения для конкурентной работы с map

1. Использование sync.Mutex или sync.RWMutex

import "sync"

type SafeMap struct {
    mu sync.RWMutex
    m  map[int]string
}

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

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

2. Использование sync.Map (для specific use-cases)

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

  • Ключи в основном не изменяются после записи
  • Много горутин читают, но мало пишут
import "sync"

func syncMapExample() {
    var m sync.Map
    
    // Запись
    m.Store("key", "value")
    
    // Чтение
    if val, ok := m.Load("key"); ok {
        fmt.Println(val)
    }
    
    // Атомарные операции
    m.LoadOrStore("another", 42)
}

3. Шардинг (разделение мапы)

type ShardedMap struct {
    shards []*Shard
}

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

func (sm *ShardedMap) getShard(key string) *Shard {
    // Используем хэш ключа для выбора шарда
    hash := fnv32(key)
    return sm.shards[hash%uint32(len(sm.shards))]
}

Ключевые выводы

  1. Мапы Go по умолчанию не потокобезопасны - это осознанное дизайнерское решение для производительности.
  2. Конкурентная запись без синхронизации = неопределённое поведение - это не просто "может сломаться", а гарантированно сломается в продакшене.
  3. Используйте примитивы синхронизации (sync.Mutex, sync.RWMutex) для защиты мап.
  4. Рассмотрите sync.Map для определённых паттернов доступа.
  5. Всегда запускайте go test -race в CI/CD пайплайне для обнаружения гонок.

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