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

Что будет, если в Map будут писать несколько горутин?

1.0 Junior🔥 72 комментариев
#Конкурентность и горутины

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

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

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

Проблема конкурентной записи в Map в Go

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

Почему это происходит

В Go map не является потокобезопасной (thread-safe) структурой данных. Причина в том, что внутренняя реализация map включает сложные операции с указателями и перераспределением памяти, которые не синхронизированы для конкурентного доступа.

Пример опасного кода

package main

import (
    "sync"
)

func main() {
    m := make(map[int]int)
    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(key int) {
            defer wg.Done()
            m[key] = key * 10 // Опасная конкурентная запись
        }(i)
    }
    
    wg.Wait()
    // Программа может упасть с panic: concurrent map writes
}

Решения проблемы

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

package main

import (
    "sync"
)

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

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

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

func main() {
    sm := &SafeMap{
        data: make(map[int]int),
    }
    
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(key int) {
            defer wg.Done()
            sm.Set(key, key*10)
        }(i)
    }
    wg.Wait()
}

2. Использование sync.Map (для специфических случаев)

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

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

import (
    "sync"
    "fmt"
)

func main() {
    var sm sync.Map
    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(key int) {
            defer wg.Done()
            sm.Store(key, key*10)
        }(i)
    }
    
    wg.Wait()
    
    // Чтение значения
    if value, ok := sm.Load(5); ok {
        fmt.Printf("Key 5: %v\n", value)
    }
}

3. Шардирование (разделение карты на части)

package main

import (
    "sync"
)

const shardCount = 32

type ShardedMap []*Shard

type Shard struct {
    mu    sync.RWMutex
    items map[int]int
}

func NewShardedMap() ShardedMap {
    m := make(ShardedMap, shardCount)
    for i := 0; i < shardCount; i++ {
        m[i] = &Shard{items: make(map[int]int)}
    }
    return m
}

func (m ShardedMap) getShard(key int) *Shard {
    return m[key%shardCount]
}

func (m ShardedMap) Set(key, value int) {
    shard := m.getShard(key)
    shard.mu.Lock()
    defer shard.mu.Unlock()
    shard.items[key] = value
}

Рекомендации по выбору решения

Когда использовать sync.Mutex/RWMutex:

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

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

  • При преимущественно read-heavy нагрузке
  • Когда ключи стабильны и редко удаляются
  • В специализированных high-load сценариях

Когда использовать шардирование:

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

Важные нюансы

  1. Чтение из map тоже требует синхронизации, если одновременно могут происходить записи
  2. Итерация по map во время конкурентной записи также опасна
  3. Даже операции, которые кажутся атомарными (например, проверка существования ключа перед записью), требуют синхронизации
// Опасный код - гонка данных
if _, exists := m[key]; !exists {
    m[key] = value // Между проверкой и записью другая горутина могла изменить map
}

// Безопасный код
mu.Lock()
if _, exists := m[key]; !exists {
    m[key] = value
}
mu.Unlock()

Вывод

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

Что будет, если в Map будут писать несколько горутин? | PrepBro