Как сделать Map потокобезопасным в Go?

Ответ

В Go встроенный тип map не является потокобезопасным. Для безопасного доступа к map в конкурентной среде используйте sync.RWMutex или sync.Map.

1. Использование sync.RWMutex (Read-Write Mutex):

sync.RWMutex предоставляет раздельные блокировки для чтения (RLock()) и записи (Lock()). Это позволяет множеству горутин читать map одновременно, но блокирует все операции (чтение и запись) при выполнении записи.

package main

import (
    "fmt"
    "sync"
    "time"
)

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

// NewSafeMap создает новый потокобезопасный Map
func NewSafeMap() *SafeMap {
    return &SafeMap{
        data: make(map[string]interface{}),
    }
}

// Get безопасно извлекает значение по ключу
func (m *SafeMap) Get(key string) (interface{}, bool) {
    m.RLock() // Блокировка для чтения
    defer m.RUnlock() // Разблокировка после завершения чтения
    val, ok := m.data[key]
    return val, ok
}

// Set безопасно устанавливает значение по ключу
func (m *SafeMap) Set(key string, value interface{}) {
    m.Lock() // Блокировка для записи
    defer m.Unlock() // Разблокировка после завершения записи
    m.data[key] = value
}

// Delete безопасно удаляет значение по ключу
func (m *SafeMap) Delete(key string) {
    m.Lock()
    defer m.Unlock()
    delete(m.data, key)
}

// Len возвращает количество элементов в Map
func (m *SafeMap) Len() int {
    m.RLock()
    defer m.RUnlock()
    return len(m.data)
}

func main() {
    safeMap := NewSafeMap()

    var wg sync.WaitGroup

    // Запись данных из нескольких горутин
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", id)
            value := fmt.Sprintf("value%d", id)
            safeMap.Set(key, value)
            fmt.Printf("Goroutine %d: Set %s = %sn", id, key, value)
        }(i)
    }

    // Чтение данных из нескольких горутин
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(10 * time.Millisecond) // Даем время для записи
            key := fmt.Sprintf("key%d", id*2) // Читаем некоторые ключи
            val, ok := safeMap.Get(key)
            if ok {
                fmt.Printf("Goroutine %d: Get %s = %vn", id, key, val)
            } else {
                fmt.Printf("Goroutine %d: Key %s not foundn", id, key)
            }
        }(i)
    }

    wg.Wait()
    fmt.Printf("nFinal map size: %dn", safeMap.Len())
    val, ok := safeMap.Get("key5")
    if ok {
        fmt.Printf("Final check: key5 = %vn", val)
    }
}

2. Использование sync.Map:

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

  • Когда ключи стабильны (редко меняются): sync.Map эффективна, когда набор ключей относительно статичен, и большинство операций — это чтения.
  • Много конкурентных чтений: Она обеспечивает высокую производительность при большом количестве одновременных операций чтения.
  • Разные горутины пишут в разные ключи: Если разные горутины обычно записывают в разные части карты, sync.Map может быть быстрее, чем RWMutex, так как она минимизирует конфликты.

sync.Map имеет методы Load, Store, LoadOrStore, Delete и Range.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store (запись)
    m.Store("name", "Alice")
    m.Store("age", 30)

    // Load (чтение)
    if val, ok := m.Load("name"); ok {
        fmt.Printf("Name: %vn", val)
    }

    // LoadOrStore (загрузить или сохранить, если отсутствует)
    // Если 'city' нет, сохранит 'New York' и вернет 'New York', true
    // Если 'city' есть, вернет существующее значение, false
    actual, loaded := m.LoadOrStore("city", "New York")
    fmt.Printf("City: %v, Loaded existing: %tn", actual, loaded)

    actual, loaded = m.LoadOrStore("age", 31) // 'age' уже есть
    fmt.Printf("Age: %v, Loaded existing: %tn", actual, loaded)

    // Delete (удаление)
    m.Delete("age")
    if _, ok := m.Load("age"); !ok {
        fmt.Println("Age deleted.")
    }

    // Range (итерация по элементам)
    fmt.Println("nIterating over map:")
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("Key: %v, Value: %vn", key, value)
        return true // Возвращаем true для продолжения итерации
    })
}

Выбор между sync.RWMutex и sync.Map:

  • Используйте sync.RWMutex для общего случая, когда вам нужен полный контроль над структурой данных и когда операции записи относительно часты или конфликтуют.
  • Используйте sync.Map для специфических сценариев, где преобладают чтения, ключи стабильны, и вы хотите избежать накладных расходов на блокировки для каждого чтения.