Найти тему

#22 Когда использовать sync.Map в Go

Оглавление

map в Go не потокобезопасны и требуют защиты через мьютексы при конкурентном доступе. В отличие от них, sync.Map обеспечивает безопасность при конкурентном доступе без необходимости явно использовать мьютексы.

Основные методы sync.Map

Delete(key any): удаляет элемент по ключу;

Load(key any) (value any, ok bool): извлекает значение по ключу;

LoadAndDelete(key any) (value any, loaded bool): извлекает и одновременно удаляет элемент;

LoadOrStore(key, value any) (actual any, loaded bool): извлекает значение или сохраняет новое, если ключ отсутствует;

Range(f func(key, value any) bool): выполняет функцию для каждого элемента мапы;

Store(key, value any): сохраняет пару ключ-значение.

cache contention

В коде sync.RWMutex можно увидеть, что при блокировке на чтение, каждая горутина атомарно обновляет счетчик readerCount. Когда ядро процессора обновляет счётчик, оно сбрасывает кеш для этого адреса в памяти для всех остальных ядер и объявляет, что владеет актуальным значением для адреса. Следующее ядро, прежде чем обновить счётчик, сначала вычитывает это значение из кеша другого ядра.

Передача между L2 кешем занимает ~ 40 нс. Когда много ядер одновременно пытаются обновить счётчик, то каждое становится в очередь и ждёт инвалидацию и вычитывание из кеша. Операция, которая должна укладываться в константное время внезапно становится O(N) по количеству ядер. Это и есть cache contention.

Для этой конкретной проблемы, когда у нас много ядер и система высоконагружена, можно использовать sync.Map вместо стандартной map с RWMutex. В остальных случаях sync.Map не нужен.

sync.Map в борьбе с Cache Contention

sync.Map оптимизирован для использования при частых чтениях и редких записях, что является обычной ситуацией в кэш-системах.

метод LoadOrStore позволяет избежать дополнительной блокировки при проверке наличия ключа и его сохранении. Если ключ уже существует, метод вернет его значение, в противном случае сохранит новое значение без риска конкурентного доступа

Рассмотрим пример частотной мапы, которая подсчитывает, сколько раз каждый ключ был запрошен:

Метод LoadOrStore обеспечивает добавление нового ключа или возврат существующего без необходимости дополнительной блокировки, что снижает риск возникновения cache contention.