← Назад к вопросам
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
| Операция | RWMutex | Mutex |
|---|---|---|
| Несколько читателей | ✅ параллельно | ❌ последовательно |
| Писатель + читатели | ✅ ждут | ✅ ждут |
| Производительность чтения | ✅ лучше | ❌ хуже |
| Производительность записи | ✅ нормально | ✅ нормально |
| Сложность кода | ✅ нормально | ✅ проще |
Сравнение реализаций
Обычный 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 нагрузки
Ключевые выводы
- RWMutex предпочтительнее Mutex для read-heavy нагрузок
- RLock() для чтения, Lock() для записи
- defer Unlock() гарантирует освобождение блокировки
- sync.Map — встроенная альтернатива
- Double-check locking для оптимизации Get-или-Set
Это — стандартный паттерн для потокобезопасных структур данных в production коде.