Что произойдет при одновременной записи в `map` из нескольких горутин и как этого избежать?

Ответ

При одновременной записи (или записи и чтении) в map из нескольких горутин произойдет состояние гонки (race condition), которое с высокой вероятностью приведет к панике (fatal error: concurrent map writes) и аварийному завершению программы.

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

Стандартная map в Go не является потокобезопасной. Это сделано намеренно для максимальной производительности в однопоточных сценариях. Внутренние операции с map, такие как перехеширование или добавление элемента, могут изменять её структуру (например, перемещать бакеты). Если одна горутина начнет запись в момент, когда другая изменяет эту структуру, целостность данных будет нарушена.

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

func main() {
    m := make(map[int]string)

    // Две горутины одновременно пишут в одну и ту же мапу
    go func() {
        for i := 0; ; i++ {
            m[i] = "a"
        }
    }()

    go func() {
        for i := 0; ; i++ {
            m[i] = "b"
        }
    }()

    // Программа упадет с паникой
    select {}
}

Как решить проблему?

Для безопасной работы с map в конкурентной среде используются примитивы синхронизации:

  1. sync.RWMutex (предпочтительный способ): Позволяет множественные одновременные чтения, но запись делает эксклюзивной. Это идеальный вариант, если у вас гораздо больше чтений, чем записей.

    var mu sync.RWMutex
    var m = make(map[int]int)
    
    // Безопасная запись
    mu.Lock()
    m[1] = 42
    mu.Unlock()
    
    // Безопасное чтение
    mu.RLock()
    _ = m[1]
    mu.RUnlock()
  2. sync.Map: Специализированный тип, оптимизированный для двух конкретных случаев:

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

    В общих случаях sync.Map может быть медленнее, чем map с RWMutex.