go语言原生的map并不是线程安全的一种数据结果,如果想要达到安全则需要使用锁,如果map比较大,则加锁解锁代价相对比较大,常见的做法拆分map,使用key值hash的方式进行小规模的锁操作,前者性能影响较大,后者使用锁较多,容易出错
go在1.9之后提供了sync.Map,一种并发安全的map数据结构
操作方式跟常见的map大同小异,只是在遍历的时候操作不太一样
func main() {
var m sync.Map
// 1. 写入
m.Store("test", 18)
// 2. 读取
age, _ := m.Load("test")
fmt.Println(age.(int))
// 3. 遍历
m.Range(func(key, value interface{}) bool {
name := key.(string)
age := value.(int)
fmt.Println(name, age)
return true
})
// 4. 删除
m.Delete("test")
age, ok := m.Load("test")
fmt.Println(age, ok)
// 5. 读取或写入
m.LoadOrStore("test", 100)
age, _ = m.Load("test")
fmt.Println(age)
}
这里面涉及到一个点在于sync.map适用的场景在于读多写少的情况,如果应用场景写入比较多,则会导致read缓冲读取失败,需要加锁,就会导致冲突增多,同时会提升misses(下面会介绍misses的概念),导致dirty提升为read,这是一个O(n)的过程导致性能会大幅度降低
看下官网给出的sync.map的源码type Map struct {
// 当涉及到dirty数据的操作的时候,需要使用这个锁
mu Mutex
// 一个只读的数据结构,因为只读,所以不会有读写冲突。
// 所以从这个数据中读取总是安全的。
// 可以并发地读。但如果需要更新 read,则需要加锁保护。对于 read 中存储的 entry 字段,可能会被并发地 CAS 更新。但是如果要更新一个之前已被删除的 entry,则需要先将其状态从 expunged 改为 nil,再拷贝到 dirty 中,然后再更新
read atomic.Value // 数据对象为readOnly
// dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
// 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
// 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
dirty map[interface{}]*entry
// 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
// 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
misses int
}
真正存储 key/value 的是 read 和 dirty 字段。read 使用 atomic.Value,这是 lock-free 的基础,保证 load/store 的原子性。dirty 则直接用了一个原始的 map,对于它的 load/store 操作需要加锁。
read 字段里实际上是存储的是:
// readOnly 主要用于存储,通过院子操作存储在Map.read里面的元素
type readOnly struct {
// read 的map数据,用户存储所有read的数据
m map[interface{}]*entry
// 如果当前元素在dirty中但是没有在read中,则该值为true,元素状态的一种标识符
amended bool
}
在dirty和read中元素侧存储数据结构都为 entry
// entry 为map.dirty中存储的具体元素值
type entry struct {
// p有是三种状态
// nil 调用delete的时候,删除,将read中元素设置为nil
// expunged 这也是一种删除状态,只有在read中存在而在dirty中没有,这种情况下降read复制到dirty中,复制的时候先将nil标记为expunged,然后不将其复制到dirty(在创建新的key的时候,如果read中存在,而dirty为空,则需要把read复制到dirty中)
p unsafe.Pointer // *interface{}
}
map的数据结构示意图
接下来看下整个的sync.map(以下简称map)的详细介绍
这是map的使用整体流程图,简单就是说写更新dirty,读从read中读,如果read中没有,则从dirty中读取
更新函数Store
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
// 如果 read map 中存在该 key 则尝试直接更改(由于修改的是 entry 内部的 pointer,因此 dirty map 也可见)
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// 如果 read map 中存在该 key,但 p == expunged,则说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值 此时:
// a. 将 p 的状态由 expunged 更改为 nil
// b. dirty map 插入 key
m.dirty[key] = e
}
// 更新 entry.p = value (read map 和 dirty map 指向同一个 entry)
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// 如果 read map 中不存在该 key,但 dirty map 中存在该 key,直接写入更新 entry(read map 中仍然没有这个 key)
e.storeLocked(&value)
} else {
// 如果 read map 和 dirty map 中都不存在该 key,则:
// a. 如果 dirty map 为空,则需要创建 dirty map,并从 read map 中拷贝未删除的元素到新创建的 dirty map
// b. 更新 amended 字段,标识 dirty map 中存在 read map 中没有的 key
// c. 将 kv 写入 dirty map 中,read 不变
if !read.amended {
// 到这里就意味着,当前的 key 是第一次被加到 dirty map 中。
// store 之前先判断一下 dirty map 是否为空,如果为空,就把 read map 浅拷贝一次。
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
// 写入新 key,在 dirty 中存储 value
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
整体过程
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 因read只读,线程安全,优先读取
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果没在 read 中找到,并且 amended 为 true,即 dirty 中存在 read 中没有的 key
if !ok && read.amended {
m.mu.Lock() // dirty map 不是线程安全的,所以需要加上互斥锁
// double check。避免在上锁的过程中 dirty map 提升为 read map。
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 仍然没有在 read 中找到这个 key,并且 amended 为 true
if !ok && read.amended {
e, ok = m.dirty[key] // 从 dirty 中找
// 不管 dirty 中有没有找到,都要"记一笔",因为在 dirty 提升为 read 之前,都会进入这条路径
m.missLocked()
}
m.mu.Unlock()
}
if !ok { // 如果没找到,返回空,false
return nil, false
}
return e.load()
}
整体过程
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
// dirty map 晋升
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
直接将 misses 的值加 1,表示一次未命中,如果 misses 值小于 m.dirty 的长度,就直接返回。否则,将 m.dirty 晋升为 read,并清空 dirty,清空 misses 计数值。这样,之前一段时间新加入的 key 都会进入到 read 中,从而能够提升 read 的命中率。
entry的load方法
func (e *entry) load() (value interface{}, ok bool) {
p := atomic.LoadPointer(&e.p)
//对于 nil 和 expunged 状态的 entry,直接返回 ok=false;否则,将 p 转成 interface{} 返回。
if p == nil || p == expunged {
return nil, false
}
return *(*interface{})(p), true
}
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果 read 中没有这个 key,则去dirty中寻找,这里使用的amended,当read和dirty不同时为true,说明dirty中有read的数据
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) // 直接从 dirty 中删除这个 key
}
m.mu.Unlock()
}
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中进行查找,如果有则直接执行delete,将p置为nil,这样read和dirty都能看到这个变化,如果read中没有找到,切dirty不为空,就需要从dirty中进行删除,从dirty中也是更新entry的状态。
如果当前可以在read和dirty中同时存在,则删除只是做一个标记,将p置为nil,而如果仅在dirty中存在时,会直接删除可以,原因在于如果两者同时存在,仅做删除标记,在下次查询的时候,仍然需要先去read查找,之后再去dirty里面进行寻找删除,增大成本
流程图
函数LoadOrStore
该函数结合了Load和Store功能,如果map中存在则返回key对应的value,否则存入map,执行顺序是先执行Load查找key是否存在,之后更新key对应的value,因为LoadOrStore可以并发执行,所以效率相对比较高
函数Range
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
if read.amended {
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()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
Range遍历当前调用时刻的map中的所有k-v,将他们传递给函数f,进行遍历操作,常见做法如下
//遍历sync.Map, 要求输入一个func作为参数
f := func(k, v interface{}) bool {
//这个函数的入参、出参的类型都已经固定,不能修改
//可以在函数体内编写自己的代码,调用map中的k,v
fmt.Println(k,v)
return true
}
m.Range(f)
总结