go sync.Map 源码分析

sync.Map 源码分析

  1. sync.Map 结构
type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int // 未命中计数器
}

type readOnly struct {
    m map[interface{}]*entry
    amended bool  // 为true时表示 Map.dirty 中是否包含 m 中不存在的 key
}
  • 从源码中可以看出, Map 结构体中包含2个原生map, 分别是 Map.dirty 和 Map.read
  • Map.read 是个readOnly 类型的结构体,其内包含一个 map 和Map.dirty 一样,都是key到*value的映射
  • Map.read 这个字典是只读的, 查询不需要加锁,只读字典不会增减其中的键值,但是可以修改键对应的值
  1. sync.Map.Load 取数据
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()
		
		// 加锁后重复查找 Map.read 的原因是,防止在加锁之前,
		// Map.dirty 提升成了 Map.read,这样的话,直接去查Map.dirty肯定也查不到
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			// 累加未命中计数器,并检查Map.dirty是否需要升级为Map.read,
			// 如需要则升级,否则,直接返回
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
  • Load 时,先去 Map.read 字典中查找,如果存在,则返回对应值,如果不存在,加锁在查一遍,还是不存在,再去Map.dirty中查找
  • 第二次查找 Map.read 的原因是,反正在加锁之前,Map.dirty 提成成了 Map.read,这样的话,直接去查Map.dirty肯定也查不到,就会造成bug
  • 无论 Map.dirty 中是否存在对应的键,都会调用missLocked函数
  1. Map.missLocked 这个函数是在加锁的状态下执行的
func (m *Map) missLocked() {
    // 未命中数加1
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	
	// 当未命中数 大于等于 dirty 的长度是, dirty 变成 m.read
	m.read.Store(readOnly{m: m.dirty})
	// 清空 dirty 和 未命中计数器
	m.dirty = nil
	m.misses = 0
}
  • 首先,未命中数加1
  • 当未命中数小于Map.dirty的长度时,直接返回,否则,将Map.dirty提升为Map.read,并将Map.dirty和Map.misses重置
  1. Map.Store 存数据
func (m *Map) Store(key, value interface{}) {
    
    // 先去Map.read中检查key是否存在,
    // 若存在,则直接修改key的值为value并返回(Map.read的key不能修改,value是可以修改的)
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
    
    // Map.read中不存在, 需要加锁去Map.dirty中查找
	m.mu.Lock()
	// 这里和Load的逻辑一样,加锁后再次查找,防止加锁之前Map.dirty提升为Map.read
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
	    // 加锁后再Map.read中查到了, 需要修改这个key对应的值
		if e.unexpungeLocked() {
			// 该key不在Map.dirty上, 在Map.dirty上保存该key
			m.dirty[key] = e
		}
		// 修改key对应的值
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
	    // 在Map.dirty中查到了该key,直接修改即可,这里用到的是原子操作
		e.storeLocked(&value)
	} else {
	    // read未被修正过
		if !read.amended {
			// 使用Map.read的键值初始化Map.dirty
			m.dirtyLocked()
			// 标记Map.read不完整, 意思就是Map.read没有包含全部的key和value,
			// Map.read的key不能修改, 只能通过 Map.dirty 升级来替换 Map.read
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		// 将值存入Map.dirty, 因为这个key在Map.read中不存在,所以上边会把Map.read标记为不完整
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}
  1. Map.LoadOrStore 取或者存数据
  • 该函数的逻辑相当于是综合了Load函数和Store函数, 这里就不赘述了
  1. Map.Delete 删除数据
func (m *Map) Delete(key interface{}) {
	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]
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete()
	}

  • 首先去 Map.read 中查询 key 是否存在,
  • 若 key 不存在, 并且 Map.read 是不完整的,则加锁重复查找,找到执行delete,
  • 找不到就直接在Map.dirty上执行delete操作
  1. Map.Range 遍历
func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	if read.amended {
		// 如果Map.read是不完整的,即Map.dirty中存在Map.read中不存在的key,则将Map.dirty升级为Map.read
		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()
	}
    
    // 迭代Map.read, 执行传入函数,当函数返回false时,结束迭代
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}
  • 该方法可以用来迭代Map中所有的元素, Map 不能直接用于for做迭代操作,要想遍历Map, 只能用该函数
  • 该方法接收一个函数,该函数接收两个参数,分别是Map的key和value
  • 迭代Map.read,当函数返回false时,结束迭代操作
  1. 总结
  • sync.Map 并发安全的map
  • sync.Map类型在内部使用了大量的原子操作来存取键和值,并使用了两个原生的map作为存储介质
  • 其中一个原生map存储在sync.Map.read字段中,该字段是sync.atomic.Value类型的,该类型可以使用原子操作,所以访问read时不需要加锁
  • 另一个原生map存储在sync.Map.dirty字段中,该字段的key类型是interface{}, value类型是*entry, 这是一个自定义的结构体,里面是一个指针类型的字段p
  • 两个原生的map中存放的value都是真正值的指针,所以两份map的底层指向的是同一份数据,只是各自保存一份指针而已
  • read的中的key是只读的,不能被修改,但是可以通过key修改value
  • 无论是查询还是修改,都是先去read中操作,只有当read中不存在key时,才会去dirty中操作
  • read中存在的key,dirty中一定存在,反之,就不一定了
  • 每次取read中查询数据,如果差不到,不管 dirty中是否存在,sync.Map.misses(未命中数)都会加1
  • sync.Map.misses大于等于dirty的长度时,将dirty升级成read,同时将sync.Map.misses置为0,dirty置为nil
  • 存储新值的时候,如果dirty为nil, 则会遍历 read 赋值给 dirty,然后将新值存入 dirty, 并标记 read.amendedtrue (意思是dirty中存在read中不存在的值)
  • 删除时,直接在dirty上执行delete函数,如果read中存在这个key,这将这个值置为nil(相当于是标记删除,read 中的key不能被修改)
  1. sync.Map 中的 read 与 dirty
    sync.Map 中的 read 与 dirty
    (图出自 极客时间-go语言36讲)

  2. sync.Map 中 read 与 dirty 的互换
    sync.Map 中 read 与 dirty 的互换
    (图出自 极客时间-go语言36讲)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值