Тип sync.Map

Тип sync.Map предоставляет потокобезопасное хранилище ключ-значение, которое можно безопасно использовать из разных горутин без дополнительной синхронизации. В отличие от обычной мапы, sync.Map не требует использования мьютексов для конкурентного доступа. Он оптимизирован для двух сценариев: когда ключи используются только один раз (кэши) и когда в конкурентной среде много операций чтения и записи.

Основные методы sync.Map: Store для сохранения значения, Load для получения значения по ключу, Delete для удаления ключа, LoadOrStore для атомарной загрузки или сохранения, LoadAndDelete для атомарной загрузки и удаления, Swap для атомарной замены значения, CompareAndSwap для атомарной условной замены, CompareAndDelete для атомарного условного удаления и Range для перебора всех элементов.

Создание sync.Map

Создать пустой sync.Map очень просто - достаточно объявить переменную без инициализации:

gopackage main

import (
  "fmt"
  "sync"
)

func main() {
  var m sync.Map
  fmt.Println(m)
}

Результат выполнения кода:

go{}

В отличие от обычных мап, sync.Map не требует инициализации через make, так как его нулевое значение уже готово к использованию.

Сохранение и получение значений

Для сохранения значения используется метод Store, который принимает ключ и значение. Для получения значения используется метод Load, который возвращает значение и флаг успешности операции:

gopackage 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")
  }
}

Результат выполнения кода:

go"Found: Alice"
"Key not found"

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

Атомарные операции с проверкой

Метод LoadOrStore позволяет атомарно выполнить операцию: если ключ существует, он возвращает существующее значение, если нет - сохраняет новое значение и возвращает его. Метод возвращает значение и флаг, указывающий, было ли значение загружено (true) или сохранено (false):

gopackage 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)
}

Результат выполнения кода:

go"Value: initial Loaded: false"
"Value: initial Loaded: true"

В первом случае ключа не было, поэтому значение было сохранено, и loaded вернул false. Во втором случае ключ уже существовал, поэтому значение не изменилось, и loaded вернул true.

Атомарная загрузка и удаление

Метод LoadAndDelete атомарно загружает значение по ключу и удаляет его из мапы. Он возвращает значение и флаг, указывающий, существовал ли ключ:

gopackage 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)
}

Результат выполнения кода:

go"Value: value Loaded: true"
"Value: <nil> Loaded: false"

При первом вызове ключ существовал, поэтому значение было загружено и удалено. При втором вызове ключа уже не было, поэтому loaded вернул false.

Использование в конкурентной среде

Главное преимущество sync.Map - это возможность безопасного использования из множества горутин без дополнительной синхронизации. Рассмотрим пример конкурентного увеличения счетчиков:

gopackage 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
  })
}

Результат выполнения кода:

go"goroutine-0: 400"
"goroutine-1: 300"
"goroutine-2: 300"

В этом примере 10 горутин конкурентно обновляют значения в sync.Map, и нам не нужно использовать мьютексы для синхронизации доступа. Метод Range используется для безопасного перебора всех элементов мапы.

Перебор всех элементов

Метод Range позволяет перебрать все пары ключ-значение в sync.Map. Он принимает функцию-колбэк, которая вызывается для каждого элемента. Если колбэк возвращает false, перебор прекращается:

gopackage 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
  })
}

Результат выполнения кода:

go"All elements:"
"  5: five"
"  1: one"
"  4: four"
"  2: two"
"  3: three"
"First three elements:"
"  5: five"
"  1: one"
"  4: four"

Важно отметить, что порядок перебора в Range не гарантируется и может меняться. В первом примере мы перебираем все элементы, во втором - останавливаем перебор после первых трех элементов.

Атомарная замена значения

Метод Swap атомарно заменяет значение по ключу и возвращает предыдущее значение (или nil, если ключа не было) и флаг, указывающий, существовал ли ключ:

gopackage 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)
}

Результат выполнения кода:

go"Previous: old Loaded: true"
"Current: new"
"Previous: <nil> Loaded: false"

Метод Swap полезен, когда нужно атомарно заменить значение и получить предыдущее для дальнейшей обработки.

Условные атомарные операции

Метод CompareAndSwap позволяет атомарно заменить значение только в том случае, если текущее значение равно ожидаемому. Метод возвращает true, если замена была выполнена:

gopackage 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)
}

Результат выполнения кода:

go"CAS success: true"
"Counter: 1"
"CAS success: false"
"Counter: 1"

Метод CompareAndDelete аналогично удаляет значение только в том случае, если текущее значение равно ожидаемому:

gopackage 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)
}

Результат выполнения кода:

go"Deleted: true"
"Key exists: false"
"Deleted: false"

Эти методы полезны для реализации оптимистичных блокировок и безопасного обновления значений в конкурентной среде.

Смотрите также

  • метод sync.Map.Store,
    который сохраняет значение по ключу
  • метод sync.Map.Load,
    который загружает значение по ключу
  • метод sync.Map.LoadOrStore,
    который атомарно загружает или сохраняет значение
  • метод sync.Map.Range,
    который перебирает все элементы мапы