go sync.Map анализ исходного кода

задняя часть Go исходный код Безопасность

Обзор

Карта на языке го не защищена от параллелизма. До версии Go 1.6 параллельное чтение и запись карты приводили к чтению грязных данных. После версии 1.6 программа вызывает панику напрямую. запись).блокировка) для обработки, Что касается того, почему go не поддерживает атомарные операции с картами, в общем, атомарные операции с картами в определенной степени снижают производительность таких сценариев, как только параллельное чтение или отсутствие одновременного чтения и записи. Однако, как сервер, если вы используете go для написания сервисов, в большинстве случаев горутина будет одновременно обращаться к картам, поэтому после версии 1.9 в пакете синхронизации go появились карты, безопасные для параллелизма. Здесь мы будем интерпретировать его из исходного кода.

1. Методы, предоставляемые sync.Map

  • Хранить данные, хранить ключ и значение можно любого типа.
func (m *Map) Store(key, value interface{})
  • удалить соответствующий ключ
func (m *Map) Delete(key interface{})
  • Прочитайте значение соответствующего ключа, ok указывает, запрошен ли ключ на карте
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
  • Для существования ключа прочтите его и сохраните, если он не существует.Если загрузка имеет значение true, это означает, что значение есть, а значение false означает, что значения нет.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
  • Указывает, что пройдены все ключи, и пройденные ключи и значения передаются в функцию обратного вызова для вызова функции.Когда функция обратного вызова возвращает false, обход заканчивается, в противном случае проходятся все ключи.
func (m *Map) Range(f func(key, value interface{}) bool)

2. Принцип

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

Кроме того, несмотря на введение двух карт, базовые данные хранят указатели, указывающие на одно и то же значение.

конкретный процесс: Например, при вставке в грязную карту ключей 1, 2 и 3 считанная карта не имеет значения ключа, при чтении она считывается с грязной карты и записывается количество промахов.

图片描述

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

图片描述

Когда вставляется новый ключ 4, значение ключа в считываемой карте копируется в грязную карту, так что грязная карта содержит все значения, а адрес копируется непосредственно при следующем обновлении до считываемой карты.

图片描述

3. Анализ исходного кода

3.1 Основная структура

Структура входа, которая используется для хранения указателя интерфейса значения, выполняет атомарные операции через atomic.

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

Структура карты, основная структура, предоставляет внешние методы и хранилище данных.

type Map struct {
	mu Mutex    

    //存储readOnly,不加锁的情况下,对其进行并发读取
	read atomic.Value // readOnly

    //dirty map用于存储写入的数据,能直接升级成read map.
	dirty map[interface{}]*entry

    //misses 主要记录read读取不到数据加锁读取read map以及dirty map的次数.
	misses int
}

Структура readOnly, в основном используемая для хранения

// readOnly 通过原子操作存储在Map.read中, 
type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

3.1 Метод загрузки

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		//加锁,然后再读取一遍read map中内容,主要防止在加锁的过程中,dirty map转换成read map,从而导致读取不到数据.
        read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			//记录miss数, 在dirty map提升为read map之前,
            //这个key值都必须在加锁的情况下在dirty map中读取到.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

3.2 Способ хранения

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
    //如果在read map读取到值,则尝试使用原子操作直接对值进行更新,更新成功则返回
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

    //如果未在read map中读取到值或读取到值进行更新时更新失败,则加锁进行后续处理
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
        //在检查一遍read,如果读取到的值处于删除状态,将值写入dirty map中
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
        //使用原子操作更新key对应的值
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
        //如果在dirty map中读取到值,则直接使用原子操作更新值
		e.storeLocked(&value)
	} else {
        //如果dirty map中不含有值,则说明dirty map已经升级为read map,或者第一次进入
        //需要初始化dirty map,并将read map的key添加到新创建的dirty map中.
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

3.3 Метод LoadOrStore

Логика кода аналогична Store.

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	// 不加锁的情况下读取read map
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
        //如果读取到值则尝试对值进行更新或读取
		actual, loaded, ok := e.tryLoadOrStore(value)
		if ok {
			return actual, loaded
		}
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
    // 在加锁的请求下在确定一次read map
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		actual, loaded, _ = e.tryLoadOrStore(value)
	} else if e, ok := m.dirty[key]; ok {
		actual, loaded, _ = e.tryLoadOrStore(value)
		m.missLocked()
	} else {
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
		actual, loaded = value, false
	}
	m.mu.Unlock()

	return actual, loaded
}

3.4 Диапазонный метод

func (m *Map) Range(f func(key, value interface{}) bool) {

    //先获取read map中值
	read, _ := m.read.Load().(readOnly)
    //如果dirty map中还有值,则进行加锁检测
	if read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
        
		if read.amended {
            //将dirty map中赋给read,因为dirty map包含了所有的值
			read = readOnly{m: m.dirty}
			m.read.Store(read)
			m.dirty = nil
			m.misses = 0
		}
		m.mu.Unlock()
	}

    //进行遍历
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

3.5 Метод удаления

func (m *Map) Delete(key interface{}) {
    //首先获取read map
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
        //加锁二次检测
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
        //没有在read map中获取到值,到dirty map中删除
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete()
	}
}

4. Ограничения

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

Для карты также существует идея реализации на основе хэша, которая состоит в том, чтобы добавить к карте блокировку чтения-записи, но выделить n карт и определить, какая карта выделена в соответствии с хеш-операцией ключа. Таким образом, потребление блокировки уменьшается до 1/n (теоретическое значение) Конкретную реализацию можно увидеть:concurrent-map

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

5. Ссылка

go
sync.map