内容:记录Go中并发安全的一种map
Go原生map不是并发安全的map
Go的原生map不是并发安全的,在多协程读写同一个map的时候,安全性无法得到保障
在Go的1.9版本之前的解决方案:使用读写锁来避免竞争
//将锁变量与map一起封装成一个并发安全的类型
var Map = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
//读数据
Map.RLock()
n := Map.m["key"]
Map.RUnlock()
//写数据
Map.Lock()
Map.m["key"]=value
Map.Unlock()
在Go的1.9版本之后,引入了并发安全的sync.Map
特点:
1、并发安全
2、通过冗余的手段,冗余两个数据结构(read、dirty)优化加锁对性能的影响
3、内部引入大量double checking(双重检测)的机制
4、动态调整,当read miss次数达到len(dirty)后,将dirty提升为read
5、延迟删除,删除某个key时,先进行逻辑删除(标记),只有dirty提升为read时才真正进行
物理删除
6、优化read的读取、更新,对read的读取不加锁
sync.Map的数据结构:
type Map struct {
//互斥锁,用于dirty数据操作
mu Mutex
//只读的数据结构,不会有读写冲突
// Entries stored in read may be updated concurrently without mu,
//but updating a previously-expunged entry requires that the entry
// be copied to the dirty
//当entries处于未删除状态(unexpunged),不需要加锁,如果entries已经删除
//(previously-expunged ),需要加锁,以便下次更新dirty
read atomic.Value // readOnly
//1、dirty数据包含了当前map最新的entries
//2、提升dirty为read时不需要一个个复制,而是利用原子操作将这个数据结构作为read
//的一部分
//3、对于dirty的操作需要加锁,对它的操作可能面临读写竞争
//4、dirty为空时,可能是刚初始化或者刚被提升,下一次操作将从read中处于未删除状态
//的数据复制到dirty中。
dirty map[interface{}]*entry
//当从read中读取数据时,如果read不包含相应的数据,将尝试加锁从dirty中读取,
//无论读取是否成功,miss均加一
//当len(dirty)==misses,将dirty提升变成read
misses int
}
该数据结构以空间换时间,冗余了read、dirty;dirty中包含read已经逻辑删除的entries,以及新加入的entries;
read对应的DataStruct为:
type readOnly struct {
m map[interface{}]*entry
//amended为true时,说明dirty中有readOnly未包含的数据,所以如果从read找不到
//数据的话,设置该值为true,要进一步到dirty中查找。
amended bool
}
实际上,尽管read、dirty之间互相冗余,但是两者数据之间通过指针指向同一块内存地址,减少了内存的浪费:
// An entry is a slot in the map corresponding to a particular key.
type entry struct {
p unsafe.Pointer // *interface{}
//p有三种值:
//1、nil: entry已被删除了,并且m.dirty为nil
//2、expunged: entry已被删除了,并且m.dirty不为nil,而且这个entry不存在于
//m.dirty中
//3、正常:entry是一个正常的有用值
}
readOnly.m和dirty存储的value类型是*entry,它包含一个指针p, 指向用户存储的value值,节约了内存的消耗。
结构图:
sync.Map各操作示意图:
sync.Map各操作源码:
## sync.Map的load函数:加载数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
//首先依赖atomic的原子操作Load获取一个只读的readOnly数据结构
read, _ := m.read.Load().(readOnly)
//查找readOnly中的数据,不需要加锁。
e, ok := read.m[key]
//如果没找到,且amended为true即dirty中有新数据,从dirty中查找,并且加锁。
if !ok && read.amended {
m.mu.Lock()
//再次获取readOnly,double-checking,避免加锁的时候m.dirty被提升为m.read,
//这个时候m.read已经不对应。
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
//不管e是否存在,misses计数均加一,当满足条件后提升dirty
m.missLocked()
}
m.mu.Unlock()
}
//找不到此元素
if !ok {
return nil, false
}
return e.load()
}
特点:
1、 一个只读的数据结构,因为只读,所以不会有读写冲突。所以从这个数据中读取总是安全的。
2、实际上,实际也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不
需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。
3、dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,
虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个
数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
4、对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
5、当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的
数据到这个数据中。
6、当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候
会将misses加一,
7、当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多
次。因为操作dirty需要加锁。
## missLocked函数:dirty如何提升为read!!!!重要
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
//当满足一定条件时,调用store函数进行提升
m.read.Store(readOnly{m: m.dirty})
//m.dirty清空
m.dirty = nil
//重新计数
m.misses = 0
}
## store函数:更新/增加一个entry
func (m *Map) Store(key, value interface{}) {
//获取readOnly
read, _ := m.read.Load().(readOnly)
//如果m.read存在这个键,并且这个entry没有被标记删除,直接存储。
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 如果read不存在或者已经被标记删除,需要操作dirty,则需要上锁
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
//如果entry被标记expunge,则表明dirty没有key,可添加入dirty,并更新entry
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() { //确保未被标记成删除,即e 指向的是非 nil 的
//标记成未被删除,往dirty中添加数据
m.dirty[key] = e
}
//更新
e.storeLocked(&value)
//dirty中存在这个k,更新数据
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
} else {
//dirty中无新的数据,往dirty中加入一个新key
if !read.amended {
//从read中复制数据
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
//将entry加入到dirty中
m.dirty[key] = newEntry(value)
}
//解锁
m.mu.Unlock()
}
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
}
}
}
特点:
1、unexpungeLocked 函数确保了 entry 没有被标记成已被清除。
2、如果 entry 先前被清除过了,那么在 mutex 解锁之前,它一定要被加入到dirty map中
3、如果 entry 的 unexpungeLocked 返回为 true,那么就说明 entry 之前被标记成了
expunged,并经过 CAS 操作成功把它置为 nil。
4、将read中未删除的数据加入到dirty中
## Delete函数:删除一个entry
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//从dirty中删除数据,直接删除
//如果read中没有,并且dirty中有新元素,那么就去dirty中去找
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
//double-checking
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
//从read删除数据,打标记
//如果read中存在该key,则将该value 赋值nil(采用标记的方式删除!)
//此时使用的是CAS,不需要加锁
if ok {
e.delete()
}
}
从read删除数据的情况:
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
//检测是否已经标记过
if p == nil || p == expunged {
return false
}
//原子操作,e.p标记为nil
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
Range函数:遍历
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
//如果m.dirty中有新数据,先升级dirty后再遍历
if read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
//double-checking双重检测
if read.amended { //dirty中有新数据,从dirty中更新到read字典来
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
}
}
}
总结:
1、 sync.Map的核心思想是用空间换时间,用两个map来存储数据(read和dirty),
read支持原子操作,操作一般不加锁,可以看作是dirty 的cache;
2、 sync.Map的4种操作
Load:先到read字典中读取,如果有则直接返回结果,如果没有或者是被删除,
则到dirty加锁中读取,如果有则返回结果,无论是否存在均更新miss
一个重要的点:将dirty提升为read的过程是先将dirty的元素加载到read中,然后将
dirty置空,计数清0,且搬的时候会看key在read是不是标记空的,是
就不需要搬过去了
Store:
更新:先到read中看看有没有,如果有且没被标记删除直接更新key,然后返回
否则上锁更新dirty中的key,注意:两个map的key的value实际指向的是同一份
增加key:直接增加到dirty中,同时标记read的map是脏的
Delete:先到read中看看有没有,如果有则直接更新为nil,如果没有,且read标记
为脏则到dirty中直接加锁删除,如果read没有且read不脏,那证明dirty中
肯定也没有,不需要去看。
还有dirty提升为read时也进行物理删除,此时会判断read中标记删除,
那么dirty中对应的键不需要拷贝过去,实现真正的删除
这些都是延迟删除的优化做法!
Range:遍历sync.Map,可能涉及提升操作,当read的脏字段是true,则需要提升,
然后遍历提升后的新map
注意:线程安全的 Map 在第一次使用之后,不允许被拷贝。