1、数据结构
- Map
type Map struct {
// 该锁用来保护dirty
mu Mutex
// 存读的数据,因为是atomic.value类型,只读类型,所以它的读是并发安全的
read atomic.Value // readOnly
//包含最新的写入的数据,并且在写的时候,会把read 中未被删除的数据
//拷贝到该dirty中,因为是普通的map存在并发安全问题,需要用到上面的mu字段
dirty map[interface{}]*entry
// 从read读数据的时候,会将该字段+1,当等于len(dirty)的时候,
//会将dirty拷贝到read中,这是性能提升的关键
misses int
}
复制代码
- readOnly
type readOnly struct {
//readOnly
m map[interface{}]*entry
// 如果Map.dirty的数据和m中的数据不一致,为true
amended bool
}
复制代码
- entry
type entry struct {
//可见value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存储的空间应该不是问题
p unsafe.Pointer // *interface{}
}
复制代码
2、API
2.1 Delete
func (m *Map) Delete(key interface{}) {
//根据key,先行访问read
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果read中没有,并且dirty中有新元素,那么就去dirty中去找
if !ok && read.amended {
m.mu.Lock()
//这是双检查(上面的if判断和锁不是一个原子性操作)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
//双检查完成之后,如果还是一样的情况下
if !ok && read.amended {
//直接删除
delete(m.dirty, key)
}
m.mu.Unlock()
}
//双检查完成之后,dirty提升了
if ok {
//如果read中存在该key,则将该value 赋值nil(采用标记的方式删除)
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
复制代码
- read中没有,并且dirty中有新元素,什么时候会出现这种情况?
- 查找(更新)时,第三分支(read、dirty都没有的情况下),为了重建dirty,会将read拷贝到dirty上,并且在dirty上面插入
2.2 Store
func(m *Map) Store(key, value interface{}) {
// 如果m.read存在这个key,并且没有被标记删除,则尝试更新
// 仅仅是尝试更新,trySrore里面如果发现被标记删除会return false
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 如果read不存在或者已经被标记删除
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
//情况一、read存在但是entry被标记expunge,则表明
//dirty里即使有,也是脏值了,使得readhedirty重新指向相同的e,
//最后在更新e
if e.unexpungeLocked() {
//加入dirty中
m.dirty[key] = e
}
//更新value值
e.storeLocked(&value)
//情况二、dirty 存在该key,更新
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
//情况三、read 和dirty都没有,新添加一条
} else {
//dirty中没有新的数据,往dirty中增加第一个新键
if !read.amended {
//将read中未删除的数据加入到dirty中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
//将read复制到dirty之后,并且仅在dirty新增
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
//将read中未删除的数据加入到dirty中
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
//read如果较大的话,可能影响性能
for k, e := range read.m {
//通过此次操作,dirty中的元素都是未被删除的,可见expunge的元素不在dirty中
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
//判断entry是否被标记删除,并且将标记为nil的entry更新标记为expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
// 将已经删除标记为nil的数据标记为expunged
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
//对entry 尝试更新
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
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
}
}
}
//read里 将标记为expunge的更新为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
//更新entry
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
复制代码
- 情况一中,为什么read为expuned时,dirty要恢复?
- 根据delete的实现可知,建立删除标记的实现是将map[key]指向nil,但是dirt[key]还是原先的那个指针(*entry)
- 等待他们都指向同一个东西的时候(尽管是nil),最后在同一赋值就好了
- 这个过程参考图示
2.3 Load
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
//因read只读,线程安全,先查看是否满足条件
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果read没有,并且dirty有新数据,那从dirty中查找,由于dirty是普通map,线程不安全,这个时候用到互斥锁了
if !ok && read.amended {
m.mu.Lock()
// 双重检查
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 如果read中还是不存在,并且dirty中有新数据
if !ok && read.amended {
e, ok = m.dirty[key]
// mssLocked()函数是sync.Map 性能得以保证的重要函数,
//目的讲有锁的dirty数据,替换到只读线程安全的read里
m.missLocked()
}
m.mu.Unlock()
}
//这里的ok,是查找完dirty[key]之后的ok
if !ok {
return nil, false
}
return e.load()
}
//dirty 提升至read 关键函数,当misses 经过多次因为load之后,大小等于len(dirty)时候,讲dirty替换到read里,以此达到性能提升。
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
//原子操作,耗时很小
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
复制代码
3、总结
-
为了理解实际实现,我们需要明白的是,无论是read还是dirty,他们存储的都是值的地址,而且他们是共享地址的。也就是说所有对read的无锁增删改查都会同步反馈在dirty上。这一点非常重要,否则你无法理解为什么增删改查没有经过dirty而dirty却始终反映最新值
-
read访问不需要加锁,dirty要
-
具体来说,应用有以下的属性,那么可以考虑使用,否则更加建议使用
RWMutex
或者Mutex
结合map的方案- 如果写入的key是稳定的(极少)
- 如果不同goroutine对key的访问是不同的