Тип sync.Map
Тип sync.Map предоставляет потокобезопасное хранилище
ключ-значение, которое можно безопасно использовать из
разных горутин без дополнительной синхронизации.
В отличие от обычной мапы, sync.Map не требует
использования мьютексов для конкурентного доступа.
Он оптимизирован для двух сценариев: когда ключи
используются только один раз (кэши) и когда в
конкурентной среде много операций чтения и записи.
Основные методы sync.Map: Store для
сохранения значения, Load для получения значения
по ключу, Delete для удаления ключа,
LoadOrStore для атомарной загрузки или сохранения,
LoadAndDelete для атомарной загрузки и удаления,
Swap для атомарной замены значения,
CompareAndSwap для атомарной условной замены,
CompareAndDelete для атомарного условного удаления
и Range для перебора всех элементов.
Создание sync.Map
Создать пустой sync.Map очень просто -
достаточно объявить переменную без инициализации:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
fmt.Println(m)
}
Результат выполнения кода:
{}
В отличие от обычных мап, sync.Map не требует
инициализации через make, так как его нулевое
значение уже готово к использованию.
Сохранение и получение значений
Для сохранения значения используется метод Store,
который принимает ключ и значение. Для получения
значения используется метод Load, который
возвращает значение и флаг успешности операции:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("name", "Alice")
m.Store("age", 30)
value, ok := m.Load("name")
if ok {
fmt.Println("Found:", value)
}
value, ok = m.Load("city")
if !ok {
fmt.Println("Key not found")
}
}
Результат выполнения кода:
"Found: Alice"
"Key not found"
Метод Load возвращает два значения: само
значение и булево значение, указывающее, был ли
найден ключ. Если ключ не найден, значение будет
нулевым для своего типа.
Атомарные операции с проверкой
Метод LoadOrStore позволяет атомарно выполнить
операцию: если ключ существует, он возвращает
существующее значение, если нет - сохраняет новое
значение и возвращает его. Метод возвращает значение
и флаг, указывающий, было ли значение загружено
(true) или сохранено (false):
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
value, loaded := m.LoadOrStore("key", "initial")
fmt.Println("Value:", value, "Loaded:", loaded)
value, loaded = m.LoadOrStore("key", "new")
fmt.Println("Value:", value, "Loaded:", loaded)
}
Результат выполнения кода:
"Value: initial Loaded: false"
"Value: initial Loaded: true"
В первом случае ключа не было, поэтому значение было
сохранено, и loaded вернул false.
Во втором случае ключ уже существовал, поэтому
значение не изменилось, и loaded вернул
true.
Атомарная загрузка и удаление
Метод LoadAndDelete атомарно загружает значение
по ключу и удаляет его из мапы. Он возвращает значение
и флаг, указывающий, существовал ли ключ:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("key", "value")
value, loaded := m.LoadAndDelete("key")
fmt.Println("Value:", value, "Loaded:", loaded)
value, loaded = m.LoadAndDelete("key")
fmt.Println("Value:", value, "Loaded:", loaded)
}
Результат выполнения кода:
"Value: value Loaded: true"
"Value: <nil> Loaded: false"
При первом вызове ключ существовал, поэтому значение
было загружено и удалено. При втором вызове ключа
уже не было, поэтому loaded вернул false.
Использование в конкурентной среде
Главное преимущество sync.Map - это возможность
безопасного использования из множества горутин без
дополнительной синхронизации. Рассмотрим пример
конкурентного увеличения счетчиков:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("goroutine-%d", id%3)
// Атомарное обновление значения
for j := 0; j < 100; j++ {
val, _ := m.LoadOrStore(key, 0)
count := val.(int) + 1
m.Store(key, count)
}
}(i)
}
wg.Wait()
m.Range(func(key, value interface{}) bool {
fmt.Printf("%s: %d\n", key, value)
return true
})
}
Результат выполнения кода:
"goroutine-0: 400"
"goroutine-1: 300"
"goroutine-2: 300"
В этом примере 10 горутин конкурентно обновляют
значения в sync.Map, и нам не нужно
использовать мьютексы для синхронизации доступа.
Метод Range используется для безопасного
перебора всех элементов мапы.
Перебор всех элементов
Метод Range позволяет перебрать все пары
ключ-значение в sync.Map. Он принимает
функцию-колбэк, которая вызывается для каждого
элемента. Если колбэк возвращает false,
перебор прекращается:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store(1, "one")
m.Store(2, "two")
m.Store(3, "three")
m.Store(4, "four")
m.Store(5, "five")
fmt.Println("All elements:")
m.Range(func(key, value interface{}) bool {
fmt.Printf(" %d: %s\n", key, value)
return true
})
fmt.Println("First three elements:")
count := 0
m.Range(func(key, value interface{}) bool {
if count >= 3 {
return false
}
fmt.Printf(" %d: %s\n", key, value)
count++
return true
})
}
Результат выполнения кода:
"All elements:"
" 5: five"
" 1: one"
" 4: four"
" 2: two"
" 3: three"
"First three elements:"
" 5: five"
" 1: one"
" 4: four"
Важно отметить, что порядок перебора в Range
не гарантируется и может меняться. В первом примере
мы перебираем все элементы, во втором - останавливаем
перебор после первых трех элементов.
Атомарная замена значения
Метод Swap атомарно заменяет значение по ключу
и возвращает предыдущее значение (или nil,
если ключа не было) и флаг, указывающий, существовал
ли ключ:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("key", "old")
prev, loaded := m.Swap("key", "new")
fmt.Println("Previous:", prev, "Loaded:", loaded)
value, _ := m.Load("key")
fmt.Println("Current:", value)
prev, loaded = m.Swap("nonexistent", "value")
fmt.Println("Previous:", prev, "Loaded:", loaded)
}
Результат выполнения кода:
"Previous: old Loaded: true"
"Current: new"
"Previous: <nil> Loaded: false"
Метод Swap полезен, когда нужно атомарно
заменить значение и получить предыдущее для
дальнейшей обработки.
Условные атомарные операции
Метод CompareAndSwap позволяет атомарно заменить
значение только в том случае, если текущее значение
равно ожидаемому. Метод возвращает true, если
замена была выполнена:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("counter", 0)
success := m.CompareAndSwap("counter", 0, 1)
fmt.Println("CAS success:", success)
value, _ := m.Load("counter")
fmt.Println("Counter:", value)
success = m.CompareAndSwap("counter", 0, 2)
fmt.Println("CAS success:", success)
value, _ = m.Load("counter")
fmt.Println("Counter:", value)
}
Результат выполнения кода:
"CAS success: true"
"Counter: 1"
"CAS success: false"
"Counter: 1"
Метод CompareAndDelete аналогично удаляет
значение только в том случае, если текущее значение
равно ожидаемому:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("key", "value")
deleted := m.CompareAndDelete("key", "value")
fmt.Println("Deleted:", deleted)
_, ok := m.Load("key")
fmt.Println("Key exists:", ok)
deleted = m.CompareAndDelete("key", "value")
fmt.Println("Deleted:", deleted)
}
Результат выполнения кода:
"Deleted: true"
"Key exists: false"
"Deleted: false"
Эти методы полезны для реализации оптимистичных блокировок и безопасного обновления значений в конкурентной среде.
Смотрите также
-
метод
sync.Map.Store,
который сохраняет значение по ключу -
метод
sync.Map.Load,
который загружает значение по ключу -
метод
sync.Map.LoadOrStore,
который атомарно загружает или сохраняет значение -
метод
sync.Map.Range,
который перебирает все элементы мапы