一 简言
1.1 文件所在目录:go/src/sync/map.go
1.2 本篇博客的go版本:go1.10.3
二 原理说明
- 空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
- 使用只读数据(read),避免读写冲突。
- 动态调整,miss次数多了之后,将dirty数据提升为read。
- double-checking(双重检测)。
- 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
- 优先从read读取、更新、删除,因为对read的读取不需要锁。
- 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