sync.map的总结

  • 我先把结论贴在前面,让人有一种大概的认知

sync.map的实现原理

dirtyLocked

一起深入

如果要详细聊这个问题,那就要说一说golang中map的问题以及为什么会出现sync.map。

map

map的问题

在go里面,map 是不支持并发写操作的,我们看下面一个例子

  • 其实slice也不是并发安全的,但 Go 的运行时只对 map 进行了检测和报错。

map的数据结构

/usr/local/Cellar/go/1.17.5/libexec/src/runtime/map.go
type bucket struct {
//next 是一个指向下一个 bucket 的指针
	next    *bucket
//allnext 是一个指向所有 bucket 的指针,用于遍历所有的 bucket。
	allnext *bucket
//typ 是一个枚举类型,表示 bucket 的类型,有 memBucket 或 blockBucket 两种
	typ     bucketType // memBucket or blockBucket (includes mutexProfile)
//hash 是一个无符号整数,表示 bucket 的哈希值,用于在哈希表中定位。
	hash    uintptr
//size 是一个无符号整数,表示 bucket 的大小,即记录数据的大小。
	size    uintptr
//nstk 是一个无符号整数,表示 bucket 的栈深度,即栈字的个数。
	nstk    uintptr
}
  • 在并发模式下保证数据安全,最基础的方式往往就是依靠锁、信号量、原子操(使用硬件级别的指令来实现不可分割的操作)作来实现,比如channle是使用了mutex来保证同一时间只有一个goroutine来写。waitgroup则是使用了信号量来控制并发安全。但是在map中我们并没有看到任何保证数据安全的操作。
  • 那么这个时候,我们就需要在map并发写入提供适当的同步机制,以确保程序的正确性和稳定性。比如我们可以这样
type MapNew struct {
	sync.RWMutex
	M map[int]int
}
  • 但是频繁的上锁解锁肯定会牺牲性能。这个时候sync.map出来了,sync.map一方面利用互斥锁来实现并发安全,另一方面,通过空间换时间的方式,通过只读的read map和可写的dirty map提高了map的读写操作。

sync.Map

sync.Map 是 Go 语言标准库中提供的一个并发安全的 Map 实现。它的设计目标是在多个 goroutine 之间共享数据时,提供高效的读写操作。

数据结构

type Map struct {
//更新dirty map和miss计算器的时候加锁
	mu Mutex
	
// 读map  读这个map里面的任何元素都是安全的,和互斥锁没有关系,但是如果要写这个map,必须要上互斥锁。
	read atomic.Value // readOnly

 最新写入的数据
	dirty map[interface{}]*entry

 计数器,每次需要读 dirty 则 +1,到达一定次数则会将写map升级为读map
	misses int
}
  • readOnly 的数据结构
type readOnly struct {
    // 内建 map
    m  map[interface{}]*entry
    // 如果写map中存在读map中不存在的key,标记为true
    amended bool
}


type entry struct {
//该指针指向真正的值的地址
//指针有三种形态,一个是p==nil,表示这个entry已经被删除了,并且m.dirty==nil
//p==expunged entry已经被清除了,但是m.dirty!=nil
    p unsafe.Pointer  // 等同于 *interface{}
}

我们再来看sync.map的四种方法

m.misses >= len(m.dirty)
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()
		// Avoid reporting a spurious miss if m.dirty got promoted while we were
		// blocked on m.mu. (If further loads of the same key will not miss, it's
		// not worth copying the dirty map for this key.)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
  • Store:存储键值对,如果键值对出现在读map中,并且不是expunged,则通过原子操作直接更新value(相同的key,读map和写map指向同一个地址),如果 read map 中没有 key 或者 entry 不能更新(可能被标记为 expunged),则需要加锁并处理三种情况:
    • 情况 1:read map 中有 key,但 entry 被标记为 expunged。这时需要先将 entry 的状态改为 nil,并拷贝到 dirty map 中,然后再更新 entry 的值。
    • 情况 2:read map 中没有 key,但 dirty map 中有 key。这时直接更新 dirty map 中的 entry 的值即可。
    • 情况 3:read map 和 dirty map 中都没有 key。这时需要先检查 read map 是否被修改过(amended 字段),如果没有,则调用 dirtyLocked 方法将 read map 拷贝到 dirty map 中(除了被标记删除的数据),并将 amended 改为 true。然后再将新的键值对存入 dirty map 中。
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			// The entry was previously expunged, which implies that there is a
			// non-nil dirty map and this entry is not in it.
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		if !read.amended {
			// We're adding the first new key to the dirty map.
			// Make sure it is allocated and mark the read-only map as incomplete.
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}
  • Delete:如果read map里面有值,则直接删,如果read map里面没有值,则加锁,并且read.amended是true(其实就是在读写map不一致,并且读map里面没有的情况下),直接删除dirty map
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
	m.LoadAndDelete(key)
}
  • Range:先判断read.amended的值,如果是false,表示readmap和dirty map 一致。如果是true,上锁,将dirty map赋值给read map,将dirty置为nil。
func (m *Map) Range(f func(key, value interface{}) bool) {
	// We need to be able to iterate over all of the keys that were already
	// present at the start of the call to Range.
	// If read.amended is false, then read.m satisfies that property without
	// requiring us to hold m.mu for a long time.
	read, _ := m.read.Load().(readOnly)
	if read.amended {
		// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
		// (assuming the caller does not break out early), so a call to Range
		// amortizes an entire copy of the map: we can promote the dirty copy
		// immediately!
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		if read.amended {
			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
		}
	}
}