go sync.map 源码分析

一 简言

1.1 文件所在目录:go/src/sync/map.go

1.2 本篇博客的go版本:go1.10.3

二 原理说明

  1. 空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
  2. 使用只读数据(read),避免读写冲突。
  3. 动态调整,miss次数多了之后,将dirty数据提升为read。
  4. double-checking(双重检测)。
  5. 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
  6. 优先从read读取、更新、删除,因为对read的读取不需要锁。
  7. read并非仅仅只读,其实更新,删除都是在read中操作的

二  简单使用

三 源码分析

基本结构体的定义

// Map就像一个map[interface{}]interface{},但是是并发安全的
// 针对两种情况进行了优化
// 1. 只写入一次,但多次读取
// 2. 多协程读取,写入,覆盖不同的key
type Map struct {
	// 锁,访问dirty时才会加锁
	mu Mutex

	// read 包含了map中可以安全并发访问的部分(使用或者不使用mu锁)
	// read 可以总是安全的load,但是store时必须和mu一起使用
	// read中的entry可以不加锁,并发地更新,但是想要更新一个之前被置为expunged的entry,那么必须加入到dirty中,保持和read一致
	read atomic.Value // 下面的readOnly对象


	// dirty包含了map中需加锁才能访问的key
	// 被置为enxgunged的entry不会保存在dirty中
	// 若dirty为nil,read中的amended为false;若dirty不为nil,read中的amended为true
	dirty map[interface{}]*entry

	// misses 统计了read中读取key不成功,需加锁后判断dirty中是否存在key的次数
	// 一旦未命中次数和dirty长度一样时,平均每个key未命中一次,也就是说加锁消耗达到了拷贝一次整个dirty,就把dirty提升为read
	misses int
}

type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true表dirty中包含read中不存在的key	 	
}

// expunged 是一个随意的指针,用来标记dirty map中的一个entry已经被删除,read中一个entry被删除时,是置为nil
var expunged = unsafe.Pointer(new(interface{}))

type entry struct {
	// p指向存储的interface值
	// 若p为nil,表明该entry在read中被删除了,但dirty中还在,所以能直接更新值
	// 若p为expunged,表明该entry不存在于dirty中(因为只有dirty新建时,p才有可能被置为expunged,具体见tryExpungeLocked函数),所以更新时要把这个entry复制到dirty中
	// 其他情况,该entry有效,且记录在read中,若dirty不为空,那么dirty也存在该entry
	// 一个entry可以通过使用原子操作替换为nil来删除
	// 一个entry对应的值,可以通过原子操作来替换
	// 若p为expunged,一个entry的对应值可以被更新,只有在第一次设置m.dirty[key] = e后

	p unsafe.Pointer // *interface{}
}

// 为i(其实是key对应的value值)新建一个entry,其中p保存的是i的地址
func newEntry(i interface{}) *entry {
	return &entry{p: unsafe.Pointer(&i)}
}

3.1 新建,修改(Store(),LoadOrStore()),源码及分析

// 存储key,value
func (m *Map) Store(key, value interface{}) {
	// 先从read中读取,存在则尝试存储,成功则返回
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	// read中不存在key,或者尝试存储时失败,加锁
	m.mu.Lock()
	// 双重检测(因为加锁过程中,dirty可能被提升为read,dirty被置为nil,此时read中可能存在该key了)
	read, _ = m.read.Load().(readOnly)
	
	if e, ok := read.m[key]; ok { // 若read中存在该key
		// 若被标记为expunged(函数内已更改为nil),说明dirty中不存在该entry(因为e.p置为expunged只有一处,即dirty时新建/重建时调用的tryExpungeLocked函数,并未添加到dirty中),所以此时需在dirty中添加该key
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}

		// 修改entry的值,注意:修改后read,dirty中都被修改了,因为read,dirty中保存的时entry的指针,这里是通过指针修改的
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok { // dirty中存在了,则更新dirty中
		e.storeLocked(&value)
	} else { // dirty中也没有该key
		// 若dirty中不包含read中不存在的key,说明dirty还未新建或重建,此时应创建dirty
		if !read.amended {
			// 新建dirty
			m.dirtyLocked()
			
			// 新建read,并把amended标记为true,因此添加该key后,dirty中就包含了read中不包含的key了
			m.read.Store(readOnly{m: read.m, amended: true})
		}

		// dirty中为该key新建一个entry条目
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

// 为该条目尝试存储值i(多协程操作,e.p的值可能有多种情况,所以需for循环使用CAS)
// 若已删除,直接返回false
// 若未删除,则循环使用CAS设置值;即使为nil,也可以赋值
func (e *entry) tryStore(i *interface{}) bool {
	p := atomic.LoadPointer(&e.p)
	// 若已删除,则直接返回
	if p == expunged {
		return false
	}
	
	// 未删除,则CAS操作,设置该值
	for {
		// CAS赋值
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}

		// 设置失败,则重新获取值
		p = atomic.LoadPointer(&e.p)
		// 若已删除,则说明其他协程刚刚删除了该值,这里不可再设置
		if p == expunged {
			return false
		}
	}
}

// 为该entry无条件存储值i(此时mu锁是锁定的,函数Locked结尾即标明;调用时e.p不能是expunged,使用原子操作赋值)
func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

// unexpungeLocked 判断本entry是否为expunged状态(此时mu锁是锁定的,判断规则见下面)
// 若该entry的value为expunged,说明已不在dirty中(因为e.p置为expunged只有一处,即dirty时新建时调用的tryExpungeLocked函数,那里并未复制到dirty中),
// 							 在mu锁解锁之前,必须要重新加入到dirty中,这里置为nil,返回true
// 若该entry的value不为expunged,则返回false
func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// 新建dirty(此时mu锁是锁定的)
func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}

	// 获取read
	read, _ := m.read.Load().(readOnly)
	
	// 新建dirty
	m.dirty = make(map[interface{}]*entry, len(read.m))
	
	// 遍历read,逐个值拷贝给dirty
	// 不能直接整个map赋值,是因为read中可能有些entry已经被标记为删除(为nil),我们只需复制那些未被删除的
	// 注意:若entry为nil,则置为expunged,标记为彻底删除,不再复制到dirty中
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

// 尝试删除本entry(此时mu锁是锁定的)
// 返回值表本entry是否已删除
func (e *entry) tryExpungeLocked() (isExpunged bool) {
	// 原子地重新读取
	p := atomic.LoadPointer(&e.p)
	
	// 指针为nil时(此时mu是锁定的,为什么还要for循环来尝试CAS呢,不懂?????)
	for p == nil {
		// 用CAS操作,置为expunged,以此来标记本条目已被彻底删除,成功则返回
		if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
			return true
		}
		// CAS失败后再获取存储的指针,若为nil,则一直尝试;若不为nil了,则跳出
		p = atomic.LoadPointer(&e.p)
	}
	
	// 若为expunged说明,已被标记删除,否则,未删除
	return p == expunged
}

// LoadOrStore	若key存在,则返回对应的value,loaded为true
// 				若key不存在,则存储给定的value值;loaded为false
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	// 先尝试从read中读取,若read中存在,则尝试loadorstore,成功则返回
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		actual, loaded, ok := e.tryLoadOrStore(value)
		if ok {
			return actual, loaded
		}
	}

	// read中不存在该key,或存在但tryLoadOrStore失败,则加锁
	m.mu.Lock()

	// 双重检测(因为加锁过程中,dirty可能被提升为read,dirty被置为nil,此时read中可能存在该key了)
	read, _ = m.read.Load().(readOnly)

	// read中已存在该key
	if e, ok := read.m[key]; ok {
		// 若为expunged状态,说明dirty中已被删除,需添加到dirty中
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		actual, loaded, _ = e.tryLoadOrStore(value) // 更新后read,dirty中都是最新的
	} else if e, ok := m.dirty[key]; ok { // dirty中存在该key
		// 在dirty中尝试loadorstore
		actual, loaded, _ = e.tryLoadOrStore(value)
		// 未命中时的处理
		m.missLocked()
	} else { // dirty中也不存在该key

		// 若此时dirty中不包含read中不存在的key,(即dirty中的key在read中都存在),说明是第一次新加key到dirty中
		if !read.amended {
			// 建立dirty
			m.dirtyLocked()
			
			// amended 置为true,因为此次添加key后,dirty中就包含了read中不存在的key
			m.read.Store(readOnly{m: read.m, amended: true})
		}

		// dirty中新建该key
		m.dirty[key] = newEntry(value)
		actual, loaded = value, false
	}
	m.mu.Unlock()

	return actual, loaded
}

注意事项:

1. Store()时,若read中有,且不为expunged,则优先在read中修改

2. 若read中不存在,dirty中存在,则修改dirty中的;若dirty中不存在,可能需要新建/重建dirty,此时amended为true

3. Store() 可以多协程环境下调用,需多注意并发问题,比如判断read是否有key的双重检测

3.2 读取(Load(),LoadOrStore()), 源码及分析

// 读取key的值
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	// 先尝试从read中读取
	e, ok := read.m[key]

	// read中不存在,且dirty中包含read中不存在的key
	if !ok && read.amended {
		// 加锁
		m.mu.Lock()
		// 双重检测(因为加锁过程中,可能dirty被提升为read,dirty被置为nil,此时read中可能有该key了)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		// read中仍然不存在该key,且dirty中包含read中不存在的key
		if !ok && read.amended {
			e, ok = m.dirty[key]
			// 无论dirty中是否存在该key,都要记录一次未命中,然后根据情况,决定是否把dirty提升为read
			m.missLocked()
		}
		m.mu.Unlock()
	}
	
	// 仍然不存在,返回
	if !ok {
		return nil, false
	}
	
	// 通过entry加载数据
	return e.load()
}

// 从entry中加载值
// 值为nil,expunged时,说明已经被删除
func (e *entry) load() (value interface{}, ok bool) {
	// 原子地重新读取值
	p := atomic.LoadPointer(&e.p)
	if p == nil || p == expunged {
		return nil, false
	}

	// 先转换为接口指针,再取值
	return *(*interface{})(p), true
}

// 未命中时的处理(此时mu锁是锁定的)
func (m *Map) missLocked() {
	m.misses++
	
	// 未命中次数没有达到dirty时,返回,即平均每个key达到未命中一次
	if m.misses < len(m.dirty) {
		return
	}

	// 把dirty提升为read,直接整个map赋值,里面的amended未赋值,即默认false,表dirty中不包含read中不存在的key,因为下面dirty就要被置为nil了
	m.read.Store(readOnly{m: m.dirty})

	m.dirty = nil
	m.misses = 0
}

// LoadOrStore	若key存在,则返回对应的value,loaded为true
// 				若key不存在,则存储给定的value值;loaded为false
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	// 先尝试从read中读取,若read中存在,则尝试loadorstore,成功则返回
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		actual, loaded, ok := e.tryLoadOrStore(value)
		if ok {
			return actual, loaded
		}
	}

	// read中不存在该key,或存在但tryLoadOrStore失败,则加锁
	m.mu.Lock()

	// 双重检测(因为加锁过程中,dirty可能被提升为read,dirty被置为nil,此时read中可能存在该key了)
	read, _ = m.read.Load().(readOnly)

	// read中已存在该key
	if e, ok := read.m[key]; ok {
		// 若为expunged状态,说明dirty中已被删除,需添加到dirty中
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		actual, loaded, _ = e.tryLoadOrStore(value) // 更新后read,dirty中都是最新的
	} else if e, ok := m.dirty[key]; ok { // dirty中存在该key
		// 在dirty中尝试loadorstore
		actual, loaded, _ = e.tryLoadOrStore(value)
		// 未命中时的处理
		m.missLocked()
	} else { // dirty中也不存在该key

		// 若此时dirty中不包含read中不存在的key,(即dirty中的key在read中都存在),说明是第一次新加key到dirty中
		if !read.amended {
			// 建立dirty
			m.dirtyLocked()
			
			// amended 置为true,因为此次添加key后,dirty中就包含了read中不存在的key
			m.read.Store(readOnly{m: read.m, amended: true})
		}

		// dirty中新建该key
		m.dirty[key] = newEntry(value)
		actual, loaded = value, false
	}
	m.mu.Unlock()

	return actual, loaded
}

// tryLoadOrStore(多协程操作,此时mu锁未锁定)
// 若该条目未被删除,原子地读取value值,loaded=true,ok=true
// 若该条目已被删除(expunged或nil),则tryLoadOrStore函数不修改,直接返回,loaded==false,ok==false
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
	p := atomic.LoadPointer(&e.p)
	
	// 已被删除,dirty中不存在该entry,不可赋值
	if p == expunged {
		return nil, false, false
	}
	
	// 正常有效值,则返回其值
	if p != nil {
		return *(*interface{})(p), true, true
	}

	// 下面是为nil值的情况
	// Copy the interface after the first load to make this method more amenable
	// to escape analysis: if we hit the "load" path or the entry is expunged, we
	// shouldn't bother heap-allocating.
	ic := i
	for {
		// CAS操作,赋值
		if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
			return i, false, true
		}
		p = atomic.LoadPointer(&e.p)
		
		// 已被删除,说明其他协程刚刚删除了,返回失败
		if p == expunged {
			return nil, false, false
		}

		// 正常值,返回成功
		if p != nil {
			return *(*interface{})(p), true, true
		}
	}
}

注意事项:

1. Load()时,若read中存在,直接读取值,返回

2. 若read中不存在,则未命中次数+1,累计次数达到dirty长度时,也就是平均每个key达到一次未命中,则把dirty提升为read

3. Load(),LoadOrStore() 可以多协程环境下调用,需多注意并发问题,比如判断read是否有key的双重检测

3.3 删除(Delete()), 源码及分析

// 删除一个key
func (m *Map) Delete(key interface{}) {
	// 优先从read中读取
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// read中不存在,但dirty中包含read中不存在的key时,需要从dirty中删除
	if !ok && read.amended {
		// 必加锁
		m.mu.Lock()
		// 双重检测(因为加锁的过程中dirty可能被提升为read,dirty被置为nil,此时read中可能有该key了)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		// 仍然不存在,但dirty中包含read中不存在的key时,从dirty中删除(在锁定中,所以可以真正删除)
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}

	// 存在时,一定是存在于read中,只需通过entry标记为已删除(置为nil)
	if ok {
		e.delete()
	}
}

// 标记一个条目删除,置为nil(mu锁未加锁,需考虑多协程安全,本entry此时存在于read中)
func (e *entry) delete() (hadValue bool) {
	for {
		// 原子地重新读取
		p := atomic.LoadPointer(&e.p)
		
		// nil 表已被其他协程通过Delete()标记删除,不再操作
		// expunged 表已被其他协程提升dirty为read时,标记删除,不再操作
		if p == nil || p == expunged {
			return false
		}

		// CAS操作,置为nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}

注意事项:

1. Delete()时,若read中存在,直接在read中标记为删除(置nil),并非真正的立即删掉,这样可以下次赋值时直接操作,达到重复使用的目的

2. 若read中存在key,标记删除后,此时dirty里面的该key是不准确的,这种不准确是临时的,因为每次往dirty中新加入key的时候,会先判断dirty是否为nil,为空则新建,且从read中筛选一遍,这样dirty又是准确的了;以后dirty被提升为read时,为nil的会被置为expunged,来标记为彻底删除,不会压入到dirty中

3. 若dirty中存在key,则彻底删除掉

4. Delete()可以多协程环境下调用,需多注意并发问题,比如判断read是否有key的双重检测,比如置为nil时的for循环CAS操作

5. 切记:dirty为nil 和 read.amended 为false,是同时存在的

    要么dirty = nil,read.amended = false,要么dirty != nil,amended = true

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值