Сила Go в одновременном чтении и записи sync.map

задняя часть Go

Всем привет, я жареная рыба.

в предыдущем "Почему карты и срезы Go не являются потокобезопасными?«В статье мы обсудили небезопасность карт и срезов в языке Go, и на основе этого мы вывели два наиболее поддерживаемых параллелизма режима карты, которые в настоящее время используются в отрасли.

Они есть:

  • Собственная карта + мьютекс или мьютекс блокировки чтения-записи.

  • Стандартная библиотека sync.Map (Go1.9 и выше).

При выборе всегда есть трудности в выборе.Как выбрать между двумя, чья производительность лучше? У меня есть друг, который сказал, что стандартная библиотека производительности sync.Map очень хороша, не используйте ее. Кого я слушаю...

Сегодня Fried Fish расскажет вам секрет Go sync.map. Сначала мы разберемся, какие сценарии используются, как использовать различные типы карт Go и у кого лучшая производительность!

Затем, по результатам анализа производительности каждой карты, исходный код sync.map анализируется целенаправленно, чтобы понять, ПОЧЕМУ.

Давайте начнем дорогу в сосание рыбы счастливо вместе.

преимущества sync.Map

Некоторые предложения по явному указанию типа карты в официальных документах Go:

图片

  • Одновременное использование несколькими горутинами безопасно и не требует дополнительной блокировки или контроля координации.

  • Большая часть кода должна использовать собственные карты вместо отдельных элементов управления блокировкой или координацией для повышения безопасности типов и удобства сопровождения.

В то же время тип карты также был оптимизирован для следующих сценариев:

  • Когда запись для данного ключа записывается только один раз, но читается несколько раз. Например, в кэше, который будет только расти, будет такой бизнес-сценарий.

  • Когда несколько горутин читают, записывают и перезаписывают записи несвязанных наборов ключей.

В обоих случаях использование типа карты может значительно снизить количество конфликтов за блокировку по сравнению с картой Go с отдельным Mutex или RWMutex.

Тестирование производительности

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

Сначала мы определяем базовую структуру данных:

// 代表互斥锁
type FooMap struct {
 sync.Mutex
 data map[int]int
}

// 代表读写锁
type BarRwMap struct {
 sync.RWMutex
 data map[int]int
}

var fooMap *FooMap
var barRwMap *BarRwMap
var syncMap *sync.Map

// 初始化基本数据结构
func init() {
 fooMap = &FooMap{data: make(map[int]int, 100)}
 barRwMap = &BarRwMap{data: make(map[int]int, 100)}
 syncMap = &sync.Map{}
}

Что касается вспомогательных методов, мы написали соответствующие методы для общих дополнений, удалений и изменений. Для последующего стресс-тестирования (показана только часть кода):

func builtinRwMapStore(k, v int) {
 barRwMap.Lock()
 defer barRwMap.Unlock()
 barRwMap.data[k] = v
}

func builtinRwMapLookup(k int) int {
 barRwMap.RLock()
 defer barRwMap.RUnlock()
 if v, ok := barRwMap.data[k]; !ok {
  return -1
 } else {
  return v
 }
}

func builtinRwMapDelete(k int) {
 barRwMap.Lock()
 defer barRwMap.Unlock()
 if _, ok := barRwMap.data[k]; !ok {
  return
 } else {
  delete(barRwMap.data, k)
 }
}

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

Базовый код метода измерения давления заключается в следующем:

func BenchmarkBuiltinRwMapDeleteParalell(b *testing.B) {
 b.RunParallel(func(pb *testing.PB) {
  r := rand.New(rand.NewSource(time.Now().Unix()))
  for pb.Next() {
   k := r.Intn(100000000)
   builtinRwMapDelete(k)
  }
 })
}

Эта часть в основном посвящена добавлению, удалению, изменению и проверке кода, а также подготовке к методу стресс-теста. Код стресс-теста непосредственно повторно используется проектом Dabai go19-examples/benchmark-for-map.

Вы также можете использовать map_bench_test.go, официально предоставленный Go, и заинтересованные партнеры могут скачать его и попробовать.

Результаты испытаний под давлением

1) Пишите:

имя метода имея в виду Результаты испытаний под давлением
BenchmarkBuiltinMapStoreParalell-4 элемент записи карты + мьютекс 237.1 ns/op
BenchmarkSyncMapStoreParalell-4 sync.map записывает элементы 509.3 ns/op
BenchmarkBuiltinRwMapStoreParalell-4 map+rwmutex записывает элементы 207.8 ns/op

При написании элементов самый медленныйsync.mapТип, за которым следует нативная карта + мьютекс (Mutex), самый быстрый — это нативная карта + блокировка чтения-записи (RwMutex).

Общий порядок (от медленного к быстрому) следующий: SyncMapStore

2) Найдите:

имя метода имея в виду Результаты испытаний под давлением
BenchmarkBuiltinMapLookupParalell-4 карта + мьютекс найти элемент 166.7 ns/op
BenchmarkBuiltinRwMapLookupParalell-4 map+rwmutex найти элемент 60.49 ns/op
BenchmarkSyncMapLookupParalell-4 sync.map находит элементы 53.39 ns/op

С точки зрения поиска элементов, самым медленным является встроенная карта + блокировка мьютекса, за которой следует собственная карта + блокировка чтения-записи. самый быстрыйsync.mapТипы.

Общий порядок следующий: MapLookup

3) Удалить:

имя метода имея в виду Результаты испытаний под давлением
BenchmarkBuiltinMapDeleteParalell-4 карта + мьютекс удалить элементы 168.3 ns/op
BenchmarkBuiltinRwMapDeleteParalell-4 map+rwmutex удаляет элементы 188.5 ns/op
BenchmarkSyncMapDeleteParalell-4 Sync.map Удалить элементы 41.54 ns/op

С точки зрения удаления элементов, самым медленным является собственная карта + блокировка чтения-записи, за которой следует собственная карта + мьютекс, самым быстрым являетсяsync.mapТипы.

Общая сортировка: RwMapDelete

Анализ сценария

Согласно приведенным выше результатам испытаний под давлением, мы можем получитьsync.MapТипы:

  • Производительность в сценариях чтения и удаления является лучшей, опережая ее более чем в два раза.

  • Производительность в сцене письма очень плохая, и она отстает от нативной карты + блокировки более чем в два раза.

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

Но если это сценарий с большим объемом записи, такой как пакетная циклическая запись с несколькими горутинами, рекомендуется найти другой способ, а производительность нельзя рассматривать напрямую (другое дело — отсутствие требований к производительности).

Анатомия sync.Map

Уметь тестировать, после результатов теста. Нам нужно копнуть глубже, чтобы понять, почему.

Зачемsync.MapТип результатов теста такой «предвзятый», почему производительность операции чтения такая высокая, а производительность операции записи ужасная, как он это спроектировал?

структура данных

sync.MapБазовая структура данных типа выглядит следующим образом:

type Map struct {
 mu Mutex
 read atomic.Value // readOnly
 dirty map[interface{}]*entry
 misses int
}

// Map.read 属性实际存储的是 readOnly。
type readOnly struct {
 m       map[interface{}]*entry
 amended bool
}

  • mu: Мьютекс, используемый для защиты чтения и загрязнения.

  • чтение: данные только для чтения, поддерживает параллельное чтение (тип atomic.Value). Если задействована операция обновления, для обеспечения безопасности данных требуются только блокировки.

  • На самом деле read хранит структуру readOnly, которая также является внутренней картой.

  • Грязные: Чтение и запись данных, это родная карта, которая не безопасна. Операция грязная требует блокировки, чтобы обеспечить безопасность данных.

  • промахи: подсчитайте, сколько раз чтение не сработало. Каждый раз, когда чтение завершается ошибкой, счетчик промахов увеличивается на 1.

В read и dirty задействованы следующие структуры:

type entry struct {
 p unsafe.Pointer // *interface{}
}

Он содержит указатель p на значение, на которое указывает элемент (ключ), сохраненный пользователем.

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

Найти процесс

Чтобы сфокусироваться, тип карты по существу имеет две «карты». Один называется read, а другой — dirty, и они похожи по длине:

图片

2 карты sync.Map

Когда мы читаем данные из типа sync.Map, он сначала проверяет, содержит ли чтение нужный элемент:

  • Если есть, данные считываются через атомарную атомарную операцию и возвращаются.

  • Если нет, то будет судread.readOnlyв исправленном свойстве он сообщит программе, если грязный содержитread.readOnly.mВ нем нет данных, поэтому, если он существует, то есть если исправление истинно, оно пойдет дальше, чтобы найти данные.

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

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

процесс записи

мы следуем прямоsync.MapМетод Store типа, который добавляет или обновляет элемент.

Исходный код выглядит следующим образом:

func (m *Map) Store(key, value interface{}) {
 read, _ := m.read.Load().(readOnly)
 if e, ok := read.m[key]; ok && e.tryStore(&value) {
  return
 }
  ...
}

передачаLoadпроверка методаm.readСуществует ли этот элемент в . Если он существует и не помечен для удаления, попробуйте сохранить.

Если элемент не существует или был помечен для удаления, перейдите к следующему процессу:

func (m *Map) Store(key, value interface{}) {
 ...
 m.mu.Lock()
 read, _ = m.read.Load().(readOnly)
 if e, ok := read.m[key]; ok {
  if e.unexpungeLocked() {
   m.dirty[key] = e
  }
  e.storeLocked(&value)
 } else if e, ok := m.dirty[key]; ok {
  e.storeLocked(&value)
 } else {
  if !read.amended {
   m.dirtyLocked()
   m.read.Store(readOnly{m: read.m, amended: true})
  }
  m.dirty[key] = newEntry(value)
 }
 m.mu.Unlock()
}

Поскольку он уже достиг грязного процесса, он вызывается непосредственно в началеLockметодверхний мьютекс, чтобы обеспечить безопасность данных, но и выделитьПервый акт плохой работы.

Он разделен на следующие три ветви обработки:

  • Если обнаруживается, что элемент существует в read, но помечен как удаленный, это означает, что dirty не равно nil (элемент не должен существовать в dirty). Он сделает следующее.

  • Измените состояние элемента с expunged на nil.

  • Вставьте элемент в грязный.

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

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

Давайте сделаем обоснование, общий поток процесса написания:

  • Проверьте прочитанное, на прочитанном нет прочитанного или отмечен статус удаления.

  • На мьютексе (Mutex).

  • Грязная операция, обработка в соответствии с различными условиями и состояниями данных.

Вернемся к исходной теме, почему его производительность при записи такая плохая. Причина:

  • Запись должна проходить через чтение, в любом случае это на один слой больше, чем другие, а состояние данных и статус нужно проверять позже, а накладные расходы на производительность относительно велики.

  • (Третья ветвь обработки) При повышении уровня инициализации или загрязнения полный объем данных будет скопирован из чтения.Если объем данных при чтении велик, это повлияет на производительность.

может быть известноsync.MapТип не подходит для сценариев, где много писать, лучше больше читать и меньше писать.

Если имеется большой объем данных, необходимо рассмотреть, допустимо ли случайное дрожание производительности при копировании данных путем чтения.

удалить процесс

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

Исходный код выглядит следующим образом:

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
 read, _ := m.read.Load().(readOnly)
 e, ok := read.m[key]
 ...
  if ok {
  return e.delete()
 }
}

Удаление — это стандартное открытие, а read по-прежнему первым проверяет, существует ли элемент.

Если есть, звонитеdeleteОтмечен как удаленный (состояние удалено), очень эффективный. Элементы в режиме чтения можно указывать и удалять, и производительность очень хорошая.

Если он не существует, он перейдет к грязному процессу:

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
 ...
 if !ok && read.amended {
  m.mu.Lock()
  read, _ = m.read.Load().(readOnly)
  e, ok = read.m[key]
  if !ok && read.amended {
   e, ok = m.dirty[key]
   delete(m.dirty, key)
   m.missLocked()
  }
  m.mu.Unlock()
 }
 ...
 return nil, false
}

Если элемент не существует при чтении, dirty не является пустым, а read несовместимо с dirty (используя исправленное суждение), это указывает на то, что dirty нужно использовать, а мьютекс заблокирован.

Повторите двойную проверку, если для чтения по-прежнему нет элемента. Затем вызовите метод удаления, чтобы отметить удаление элемента как грязного.

Следует отметить, что метод удаления встречается чаще:

func (e *entry) delete() (value interface{}, ok bool) {
 for {
  p := atomic.LoadPointer(&e.p)
  if p == nil || p == expunged {
   return nil, false
  }
  if atomic.CompareAndSwapPointer(&e.p, p, nil) {
   return *(*interface{})(p), true
  }
 }
}

Этот метод устанавливает для entry.p значение nil и помечает его как удаленное (удаленное состояние) ине совсем удалить.

Примечание: не злоупотребляйтеsync.Map, согласно случаю, которым некоторое время назад поделился Byte Boss, они поставили соединение в качестве ключа, поэтому память, связанная с этим соединением, например: буферная память никогда не может быть освобождена...

Суммировать

Прочитав эту статью, мыsync.MapСитуация с производительностью между родной картой + блокировкой мьютекса/чтения-записи.

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

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

Очень тревожно видеть, что одновременные операции чтения и записи карт приводят к фатальным ошибкам. Если вы считаете, что эта статья хороша, поделитесь ею с другими любителями го :)

Если у вас есть какие-либо вопросы, пожалуйста, оставьте отзыв и обменяйтесь мнениями в области комментариев.Лучшие отношения - это достигать друг друга, твойподобното естьжареная рыбаСамая большая мотивация для творчества, спасибо за поддержку.

Статья постоянно обновляется, вы можете выполнить поиск в WeChat, чтобы прочитать [Жареная рыба в мозгу], эта статьяGitHub GitHub.com/Vicious Genetics/Нет...Он был включен, добро пожаловать в Star, чтобы призвать больше.

Ссылаться на

  • Package sync

  • Наступил на яму Голанг sync.Map

  • go19-examples/benchmark-for-map

  • Глубокое понимание принципа работы sync.Map на примерах