深刻理解Go-sync.Map原理剖析

Map is like a Go map[interface{}]interface{} but is safe for concurrent use安全

by multiple goroutines without additional locking or coordination.markdown

Loads, stores, and deletes run in amortized constant time.并发

上面一段是官方对sync.Map 的描述,从描述中看,sync.Mapmap 很像,sync.Map 的底层实现也是依靠了map,可是sync.Map 相对于 map 来讲,是并发安全的。less

1. 结构概览

1.1. sync.Map

sync.Map的结构体了函数

type Map struct {
	mu Mutex

  // 后面是readOnly结构体,依靠map实现,仅仅只用来读
	read atomic.Value // readOnly

	// 这个map主要用来写的,部分时候也承担读的能力
	dirty map[interface{}]*entry

	// 记录自从上次更新了read以后,从read读取key失败的次数
	misses int
}
复制代码

1.2. readOnly

sync.Map.read属性所对应的结构体了,这里不太明白为何不把readOnly结构体的属性直接放入到sync.Map结构体里源码分析

type readOnly struct {
  // 读操做所对应的map
	m       map[interface{}]*entry
  // dirty是否包含m中不存在的key
	amended bool // true if the dirty map contains some key not in m.
}
复制代码

1.3. entry

entry就是unsafe.Pointer,记录的是数据存储的真实地址性能

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

复制代码

1.4. 结构示意图

经过上面的结构体,咱们能够简单画出来一个结构示意图this

2. 流程分析

咱们经过下面的动图(也能够手动debug),看一下在咱们执行Store Load Delete 的时候,这个结构体的变换是如何的,先增长一点咱们的认知atom

func main() {
	m := sync.Map{}
	m.Store("test1", "test1")
	m.Store("test2", "test2")
	m.Store("test3", "test3")
	m.Load("test1")
	m.Load("test2")
	m.Load("test3")
	m.Store("test4", "test4")
	m.Delete("test")
	m.Load("test")
}
复制代码

以上面代码为例,咱们看一下m的结构变换spa

3. 源码分析

3.1. 新增key

新增一个key value,经过Store方法来实现

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
  // 若是这个key存在,经过tryStore更新
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
  // 走到这里有两种状况,1. key不存在 2. key对应的值被标记为expunged,read中的entry拷贝到dirty时,会将key标记为expunged,须要手动解锁
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
    // 第二种状况,先解锁,而后添加到dirty
		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 {
    // m中没有,可是dirty中存在,更新dirty中的值
		e.storeLocked(&value)
	} else {
    // 若是amend==false,说明dirty和read是一致的,可是咱们须要新加key到dirty里面,因此更新read.amended
		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.
      // 这一步会将read中全部的key标记为 expunged
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}
复制代码

3.1.1. tryLock

func (e *entry) tryStore(i *interface{}) bool {
	p := atomic.LoadPointer(&e.p)
  // 这个entry是key对应的entry,p是key对应的值,若是p被设置为expunged,不能直接更新存储
	if p == expunged {
		return false
	}
	for {
    // 原子更新
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
	}
}
复制代码

tryLock会对key对应的值,进行判断,是否被设置为了expunged,这种状况下不能直接更新

3.1.2. dirtyLock

这里就是设置 expunged 标志的地方了,而这个函数正是将read中的数据同步到dirty的操做

func (m *Map) dirtyLocked() {
  // dirty != nil 说明dirty在上次read同步dirty数据后,已经有了修改了,这时候read的数据不必定准确,不能同步
	if m.dirty != nil {
		return
	}

	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[interface{}]*entry, len(read.m))
	for k, e := range read.m {
    // 这里调用tryExpungeLocked 来给entry,即key对应的值 设置标志位
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}
复制代码

3.1.3. tryExpungeLocked

经过原子操做,给entry,key对应的值设置 expunged 标志

func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := atomic.LoadPointer(&e.p)
	for p == nil {
		if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
	}
	return p == expunged
}
复制代码

3.1.4. unexpungeLocked

func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
复制代码

根据上面分析,咱们发现,在新增的时候,分为四种状况:

  1. key原先就存在于read中,获取key所对应内存地址,原子性修改
  2. key存在,可是key所对应的值被标记为 expunged,解锁,解除标记,并更新dirty中的key,与read中进行同步,而后修改key对应的值
  3. read中没有key,可是dirty中存在这个key,直接修改dirty中key的值
  4. read和dirty中都没有值,先判断自从read上次同步dirty的内容后有没有再修改过dirty的内容,没有的话,先同步read和dirty的值,而后添加新的key value到dirty上面

当出现第四种状况的时候,很容易产生一个困惑:既然read.amended == false,表示数据没有修改,为何还要将read的数据同步到dirty里面呢?

这个答案在Load 函数里面会有答案,由于,read同步dirty的数据的时候,是直接把dirty指向map的指针交给了read.m,而后将dirty的指针设置为nil,因此,同步以后,dirty就为nil

下面看看具体的实现

3.2. 读取(Load)

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
  // 若是read的map中没有,且存在修改
	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.)
    // 再查找一次,有可能刚刚将dirty升级为read了
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
      // 若是amended 仍是处于修改状态,则去dirty中查找
			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.
      // 增长misses的计数,在计数达到必定规则的时候,触发升级dirty为read
			m.missLocked()
		}
		m.mu.Unlock()
	}
  // read dirty中都没有找到
	if !ok {
		return nil, false
	}
  // 找到了,经过load判断具体返回内容
	return e.load()
}

func (e *entry) load() (value interface{}, ok bool) {
	p := atomic.LoadPointer(&e.p)
  // 若是p为nil或者expunged标识,则key不存在
	if p == nil || p == expunged {
		return nil, false
	}
	return *(*interface{})(p), true
}
复制代码

为何找到了p,可是p对应的值为nil呢?这个答案在后面解析Delete函数的时候会被揭晓

3.2.1. missLocked

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
  // 直接把dirty的指针给read.m,而且设置dirty为nil,这里也就是 Store 函数的最后会调用 m.dirtyLocked的缘由
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}
复制代码

3.3. 删除(Delete)

这里的删除并非简单的将key从map中删除

func (m *Map) Delete(key interface{}) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
  // read中没有这个key,可是Map被标识修改了,那么去dirty里面看看
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
      // 调用delete删除dirty的map,delete会判断key是否存在的
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
  // 若是read中存在,则假删除
	if ok {
		e.delete()
	}
}

func (e *entry) delete() (hadValue bool) {
	for {
		p := atomic.LoadPointer(&e.p)
    // 已是被删除了,不须要管了
		if p == nil || p == expunged {
			return false
		}
    // 原子性 将key的值设置为nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}
复制代码

根据上面的逻辑能够看出,删除的时候,存在如下几种状况

  1. read中没有,且Map存在修改,则尝试删除dirty中的map中的key
  2. read中没有,且Map不存在修改,那就是没有这个key,无需操做
  3. read中有,尝试将key对应的值设置为nil,后面读取的时候就知道被删了,由于dirty中map的值跟read的map中的值指向的都是同一个地址空间,因此,修改了read也就是修改了dirty

3.3. 遍历(Range)

遍历的逻辑就比较简单了,Map只有两种状态,被修改过和没有修改过

修改过:将dirty的指针交给read,read就是最新的数据了,而后遍历read的map

没有修改过:遍历read的map就行了

func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	if read.amended {
		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
		}
	}
}
复制代码

3.4. 适用场景

在官方介绍的时候,也对适用场景作了说明

The Map type is optimized for two common use cases:

(1) when the entry for a given key is only ever written once but read many times, as in caches that only grow,

(2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.

In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.

经过对源码的分析来理解一下产生这两条规则的缘由:

读多写少:读多写少的环境下,都是从read的map去读取,不须要加锁,而写多读少的状况下,须要加锁,其次,存在将read数据同步到dirty的操做的可能性,大量的拷贝操做会大大的下降性能

读写不一样的key:sync.Map是针对key的值的原子操做,至关于加锁加载 key上,因此,多个key的读写是能够同时并发的

相关文章
相关标签/搜索