2 мая 2020 г. Sync.map Map Concurrency
Давайте рассмотрим использование sync.Map и его исходный код.
Следующие методы доступны для работы с sync.Map:
Load(key interface{}) (value interface{}, ok bool)Store(key, value interface{})Delete(key interface{})Range(f func(key, value interface{}) bool)LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)Это покрывает все случаи использования карты — вставка, чтение, удаление и итерация. Также есть бонусный метод LoadOrStore, который позволяет установить значение по ключу, если значение не было установлено ранее.
Для выполнения итерации есть Range, который получает функцию, вызываемую для каждого элемента карты. Если эта функция возвращает false, итерация будет остановлена.
При виде названия sync.Map можно подумать, что sync.Map — это простая карта с некоторыми функциями пакета sync. Но на самом деле sync.Map намного сложнее. Давайте погрузимся в исходный код и выясним, как это работает.
Структура sync.Map выглядит следующим образом:
type Map struct {
mu sync.Mutex
read atomic.Value
dirty map[interface{}]*entry
misses int
}
type readOnly struct {
m map[interface{}]*entry
amended bool
}
read — указатель на структуру readOnly. Эта структура хранит часть данных, которые используются для чтения данных по ключу или для проверки наличия данных за этим ключом. В read нет вставок, поэтому при доступе к read не используется мьютекс.
dirty — карта, которая хранит другую часть элементов. Она обычно используется для размещения новых элементов и также для чтения. В этом случае мьютекс mu используется при доступе к dirty.
Таким образом, sync.Map имеет две внутренние карты (read и dirty). Используя одну карту только для чтения, а вторую для записи, sync.Map пытается избежать использования мьютекса, когда это возможно. Давайте рассмотрим операции sync.Map.
Сначала обращаются к read, чтобы проверить, есть ли данные за ключом. Если они есть — данные возвращаются. Это самый простой сценарий — вернуть данные из read.
Затем выполняется поиск в dirty — мьютекс mu активируется заранее. misses — счетчик обращений к dirty — увеличивается. Важная деталь — если значение этого счетчика больше количества элементов в dirty, элементы из dirty будут скопированы в read, и счетчик будет сброшен. Копирование произойдет сразу после доступа к dirty — в этот момент активируется mu.
Чтобы избежать лишнего доступа к dirty, в структуре readOnly (которая является read) есть поле amended. amended = true означает, что в sync.Map есть непустая карта dirty.
Таким образом, чтение из sync.Map наиболее эффективно, когда оно только для чтения. В этом случае это чтение равно простому чтению карты (без мьютекса). Если есть только чтение из большой карты, настроенной заранее, sync.Map хорошо подходит для этого случая.
Сначала выполняется проверка read — есть ли данные за ключом. Если да — значение обновляется с помощью atomic.CompareAndSwap.
Если данных, прочитанных по ключу, нет, выполняется то же чтение в dirty. Если dirty не инициализирована — она будет инициализирована.
Таким образом, обновление уже существующего ключа простое, но добавление данных с новым ключом сложное.
Перед итерацией dirty копируется в read, если dirty что-то хранит. Это делается с активированным мьютексом mu.
Затем выполняется итерация по read — простая итерация по карте.
sync.Map — это сложная структура, состоящая в основном из двух карт — одной для чтения и одной для новых элементов. sync.Map не только близок к map+sync.RWMutex по показателям скорости, но также может быть лучшим в случае только чтения. Когда есть и чтение, и обновление, sync.Map будет иметь элементы как в read, так и в dirty. В этом случае sync.Map работает хуже, чем map+sync.RWMutex, из-за чтения из двух внутренних карт. Кроме того, есть счетчик, который нужно обновлять при доступе к dirty.
В Go у нас есть функциональность горутин из коробки. Мы можем запускать код параллельно. Однако в нашем параллельно выполняющемся коде мы можем работать с общими переменными, и не совсем понятно, как именно Go обрабатывает такие ситуации.
Read More → Map SourcesПрограммный интерфейс map в Go описан в блоге Go. Нам просто нужно вспомнить, что map — это хранилище ключ-значение, и оно должно извлекать значения по ключу как можно быстрее.
Read More → Map Sources