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

Thread-safe Map

2.2 Middle🔥 251 комментариев
#Конкурентность и горутины#Основы Go

Условие

Реализуйте потокобезопасную (thread-safe) обёртку над map.

Интерфейс

type SafeMap struct {
    // ваши поля
}

func NewSafeMap() *SafeMap
func (m *SafeMap) Set(key string, value interface{})
func (m *SafeMap) Get(key string) (interface{}, bool)
func (m *SafeMap) Delete(key string)
func (m *SafeMap) Len() int

Требования

  • Безопасное использование из нескольких горутин
  • Использовать sync.RWMutex для оптимизации чтения
  • Set - эксклюзивная блокировка
  • Get - разделяемая блокировка для чтения

Пример

m := NewSafeMap()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        m.Set(fmt.Sprintf("key%d", i), i)
    }(i)
}
wg.Wait()

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

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

Решение

Thread-safe Map — это обёртка над стандартной map с синхронизацией доступа через RWMutex. RWMutex позволяет нескольким читателям работать одновременно, но исключает писателей.

Подход

  • Используем sync.RWMutex для синхронизации
  • Set/Delete: Lock() → эксклюзивный доступ
  • Get: RLock() → разделяемый доступ для читателей
  • Len: RLock() → читаем только размер

Реализация

package main

import (
    "fmt"
    "sync"
)

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

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

func (m *SafeMap) Set(key string, value interface{}) {
    m.mu.Lock()  // эксклюзивная блокировка для записи
    defer m.mu.Unlock()
    m.items[key] = value
}

func (m *SafeMap) Get(key string) (interface{}, bool) {
    m.mu.RLock()  // разделяемая блокировка для чтения
    defer m.mu.RUnlock()
    val, ok := m.items[key]
    return val, ok
}

func (m *SafeMap) Delete(key string) {
    m.mu.Lock()  // эксклюзивная блокировка
    defer m.mu.Unlock()
    delete(m.items, key)
}

func (m *SafeMap) Len() int {
    m.mu.RLock()  // разделяемая блокировка для чтения
    defer m.mu.RUnlock()
    return len(m.items)
}

func main() {
    m := NewSafeMap()
    var wg sync.WaitGroup
    
    // Записываем 100 значений параллельно
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m.Set(fmt.Sprintf("key%d", i), i)
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("Size: %d\n", m.Len())  // 100
    
    // Читаем значения параллельно
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            val, _ := m.Get(fmt.Sprintf("key%d", i))
            // val содержит i
        }(i)
    }
    
    wg.Wait()
}

Пошаговый пример

Операция        RWMutex поведение
────────────────────────────────────────
Set("a", 1)    Lock() → блокируют все
Get("a")       RLock() → разделяемая, можно несколько
Get("b")       RLock() → разделяемая, можно несколько
Set("c", 3)    Lock() → ждёт освобождения от Get(s)
Get("d")       RLock() → ждёт освобождения от Set()
Delete("a")    Lock() → эксклюзивная

Анализ сложности

  • Set: O(1) + время получения блокировки
  • Get: O(1) + время получения блокировки
  • Delete: O(1) + время получения блокировки
  • Len: O(1) + время получения блокировки

Полный код с примерами

package main

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

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

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

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

// Get возвращает значение (разделяемая блокировка для чтения)
func (m *SafeMap) Get(key string) (interface{}, bool) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    val, ok := m.items[key]
    return val, ok
}

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

// Len возвращает размер (разделяемая блокировка)
func (m *SafeMap) Len() int {
    m.mu.RLock()
    defer m.mu.RUnlock()
    return len(m.items)
}

func main() {
    // Пример 1: базовое использование
    fmt.Println("=== Пример 1: базовое использование ===")
    m := NewSafeMap()
    
    m.Set("name", "Alice")
    m.Set("age", 30)
    
    name, _ := m.Get("name")
    fmt.Printf("Name: %v\n", name)  // Alice
    
    m.Delete("name")
    _, ok := m.Get("name")
    fmt.Printf("Exists: %v\n", ok)  // false
    
    // Пример 2: множественные горутины
    fmt.Println("\n=== Пример 2: множественные горутины ===")
    m2 := NewSafeMap()
    var wg sync.WaitGroup
    
    // 100 писателей
    start := time.Now()
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m2.Set(fmt.Sprintf("key%d", i), i)
        }(i)
    }
    
    // 1000 читателей (параллельно с писателями)
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", i%100)
            _, _ = m2.Get(key)
        }(i)
    }
    
    wg.Wait()
    elapsed := time.Since(start)
    
    fmt.Printf("Size: %d\n", m2.Len())
    fmt.Printf("Time: %v\n", elapsed)
    
    // Пример 3: конкурирующий доступ
    fmt.Println("\n=== Пример 3: конкурирующий доступ ===")
    m3 := NewSafeMap()
    
    // Писатель
    go func() {
        for i := 0; i < 10; i++ {
            m3.Set(fmt.Sprintf("counter%d", i), i)
            time.Sleep(10 * time.Millisecond)
        }
    }()
    
    // Читатель
    go func() {
        for i := 0; i < 10; i++ {
            val, ok := m3.Get("counter5")
            fmt.Printf("Read: key=counter5, val=%v, ok=%v\n", val, ok)
            time.Sleep(15 * time.Millisecond)
        }
    }()
    
    time.Sleep(500 * time.Millisecond)
}

RWMutex vs обычный Mutex

ОперацияRWMutexMutex
Несколько читателей✅ параллельно❌ последовательно
Писатель + читатели✅ ждут✅ ждут
Производительность чтения✅ лучше❌ хуже
Производительность записи✅ нормально✅ нормально
Сложность кода✅ нормально✅ проще

Сравнение реализаций

Обычный Mutex (более простой):

type SimpleMap struct {
    mu    sync.Mutex
    items map[string]interface{}
}

func (m *SimpleMap) Get(key string) (interface{}, bool) {
    m.mu.Lock()  // ждёт, даже при чтении!
    defer m.mu.Unlock()
    val, ok := m.items[key]
    return val, ok
}

RWMutex (оптимизированный):

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

func (m *SafeMap) Get(key string) (interface{}, bool) {
    m.mu.RLock()  // разделяемая, несколько читателей
    defer m.mu.RUnlock()
    val, ok := m.items[key]
    return val, ok
}

Альтернатива: sync.Map (встроенное решение)

var m sync.Map

m.Store("key", "value")
val, ok := m.Load("key")
m.Delete("key")

// Плюсы: оптимизирована для частых читаний
// Минусы: нет метода Len(), Range сложнее использовать

Добавление метода Range

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

func (m *SafeMap) Range(fn func(key string, value interface{}) bool) {
    m.mu.RLock()  // разделяемая блокировка
    defer m.mu.RUnlock()
    
    for k, v := range m.items {
        if !fn(k, v) {
            break
        }
    }
}

func main() {
    m := NewSafeMap()
    m.Set("a", 1)
    m.Set("b", 2)
    m.Set("c", 3)
    
    m.Range(func(k string, v interface{}) bool {
        fmt.Printf("%s: %v\n", k, v)
        return true
    })
}

Добавление GetOrSet

func (m *SafeMap) GetOrSet(key string, defaultValue interface{}) interface{} {
    m.mu.RLock()  // сначала пытаемся прочитать
    if val, ok := m.items[key]; ok {
        m.mu.RUnlock()
        return val
    }
    m.mu.RUnlock()
    
    // если не нашли, устанавливаем значение
    m.mu.Lock()
    defer m.mu.Unlock()
    
    // двойная проверка (pattern: double-check locking)
    if val, ok := m.items[key]; ok {
        return val
    }
    
    m.items[key] = defaultValue
    return defaultValue
}

Производительность

Сценарий: 90% чтений, 10% записей

RWMutex:
  Читатели выполняются параллельно → быстро ✅
  Писатели ждут (редко) → нормально

Мутекс:
  Все ждут друг друга → медленнее ❌

Выигрыш RWMutex: 3-5x для read-heavy нагрузки

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

  1. RWMutex предпочтительнее Mutex для read-heavy нагрузок
  2. RLock() для чтения, Lock() для записи
  3. defer Unlock() гарантирует освобождение блокировки
  4. sync.Map — встроенная альтернатива
  5. Double-check locking для оптимизации Get-или-Set

Это — стандартный паттерн для потокобезопасных структур данных в production коде.

Thread-safe Map | PrepBro