Go语言中map是一种无须的键值对集合,可以通过key来快速检索数据。Go语言自带的标准的map类型是并发读安全的,但并发写不安全。实现一个并发读写安全的map,只需要定义一个带有RWMutex锁和map类型结构体即可。除此之外,Go语言(1.9以上版本)标准库提供了一个并发安全的集合结构体sync.Map,直接直接安全使用,无需额外加锁。
因此,本篇文章就从源码的角度分析sync.Map实现并发读写安全的原理。
在讲解sync.Map之前,我们先简单看map并发写的问题以及怎么实现并发写的安全。
package maing
// 定义变量
var sMap = make(map[interface{}]interface{}, 0)
func main() {
for i := 0; i < 100000; i += 1 {
go func(key int) {
sMap[key] = key
}(i)
}
}
运行上述代码会出现报错:"fatal error: concurrent map writes" ,开启多个协程并发写入导致。如何实现并发安全map写入呢,请看下面改良代码。
package main
import (
"sync"
)
// 定义带RWMutex和map的结构体
type safeMap = struct {
sync.RWMutex
m map[interface{}]interface{}
}
// 初始化定义变量
var sMap = safeMap{m: make(map[interface{}]interface{})}
func main() {
for i := 0; i < 100000; i += 1 {
go func(key int) {
sMap.Lock()
sMap.m[key] = key
sMap.Unlock()
}(i)
}
}
运行上传代码程序正常执行,解决map并发安全写入问题。但这种解决方式相当于串行写,在大并发写入情况会导致性能问题。所以我们用Go的官方包sync.Map,能更好的实现需求。
之前map读操作中,采用传统大锁,其锁竞争十分激烈,造成性能极具下降。sync.Map采用空间换时间,冗余数据结构,减少执行时间,提升性能。
sync.Map中冗余数据结构是read和dirty,二者存放的都是key-entry,entry其实是一个指针,指向value;read和dirty各自维护一套key,key指向的都是同一个value,即修改了entry,对read和dirty都是可见的。
sync.map通过read和dirty两个字段实现读写分离,将读和写操作分开,避免了读写操作。
当read不符合要求就操作dirty,此时会加锁,加锁后再次判断read是否符合要求(read可能在加锁期间更新,符合要求了),符合则操作后续内容,如不符合再操作dirty。
在删除操作中,只需要将要删除的k-v打上标记,这样可以让delete操作先返回,减少时间消耗;后面提升dirty时,一次性删除标记的k-v。
需要进行读取、删除、更新操作时,需要优先操作read,因为read无锁,是在read中得不到结果,再去操作dirty。
entry的指针p,是有状态的。分三种状态:nil、expunged(指向被删除的元素)、正常。
sync.Map并发安全性的实现是典型的空间换时间的思想,提供了read和dirty这两个数据结构,用来降低加锁对性能的负面影响。其结构体如下:
// Map map结构体
type Map struct {
mu Mutex // 当涉及到dirty数据的操作,需要使用此锁
read atomic.Value // readOnly
dirty map[interface{}]*entry // dirty数据结构
misses int // 计数器
}
// readOnly 是一个不可变结构,以原子方式存储在Map.read字段中
type readOnly struct {
m map[interface{}]*entry
amended bool // amended=true表示dirty map存在新值和m不一致
}
// entry 键值对中的值结构体
type entry struct {
p unsafe.Pointer // *interface{}
}
说明 | 类型 | 作用 |
mu | Mutex | 加锁作用,操作dirty时使用 |
read | atomic.Value | 只读的数据,实际数据类型为readOnly |
dirty | map[interface{}]*entry | 包括read中的数据,还有新增的数据;misses计数达到一定值,会将dirty提升为read,重置dirty和misses |
说明 | 类型 | 作用 |
m | map[interface{}]*entry | map结构 |
amended | bool | amended=true:Map.dirty和m不一致;否则相同 |
说明 | 类型 | 作用 |
p | unsafe.Pointer | sync.map中key和value是分开存放,key通过内置map指向entry,entry通过指针,指向value实际内存地址 |
// Load 查询key值是否存在
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 在read中查找key
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 在read中未找到,且read和dirty数据不一致,开始加锁
if !ok && read.amended {
// 加锁,准备操作dirty中数据
m.mu.Lock()
// 双检查机制,再次在read中查找key(此时可能read从dirty中更新了数据)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 在read中还是没有找到,且read和dirty数据仍然不一致
if !ok && read.amended {
// 在dirty中查找key
e, ok = m.dirty[key]
// read不命中次数+1,到达阈值后,为避免read命中率太低,会从dirty中更新read数据
m.missLocked()
}
// 解锁
m.mu.Unlock()
}
// 未查找到key,说明key在map中确实不存在,返回nil
if !ok {
return nil, false
}
// 查找到key,返回value
return e.load()
}
// missLocked read miss计数+1
func (m *Map) missLocked() {
m.misses++ // read 没命中次数+1
// 计数 < dirty数量,无需操作
if m.misses < len(m.dirty) {
return
}
// dirty提升为read,该操作是原子操作
m.read.Store(readOnly{m: m.dirty})
// dirty重置
m.dirty = nil
// miss计数重置
m.misses = 0
}
整体过程:
// Store 新增|更新 k-v
func (m *Map) Store(key, value interface{}) {
// 在read查找key,且entry没有被标记删除,则更新,否则返回false
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 加锁,准备操作dirty
m.mu.Lock()
// 双检查机制,再次在read中查找key
read, _ = m.read.Load().(readOnly)
// read中存在key
if e, ok := read.m[key]; ok {
// 未被标记成删除
if e.unexpungeLocked() {
// 加入dirty,此处是指针
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
if !read.amended {
// 此时key是第一次被加到dirty map中;store之前先判断dirty map是否为空,若为空,就把read map拷贝到dirty map
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
// 在dirty中存储k-v
m.dirty[key] = newEntry(value)
}
// 解锁,不再操作dirty
m.mu.Unlock()
}
整体流程:
// Delete 删除k-v
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
// LoadAndDelete
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
// 在read中查找key
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 在read中无此key,且read和dirty中数据不同
if !ok && read.amended {
// 加锁,准备操作dirty
m.mu.Lock()
// 双检查机制,在read中查询key
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 在read中未查找到key,且read和dirty中数据不相同,则在dirty中删除key
if !ok && read.amended {
e, ok = m.dirty[key]
delete(m.dirty, key)
m.missLocked()
}
// 解锁,不再操作dirty
m.mu.Unlock()
}
// 若read中找到key,则将key对应value置为nil
if ok {
// 将entry.p标记为nil,数据并没有实际删除
// 真正删除数据并被置为expunged,是在Store的tryExpungeLocked中
return e.delete()
}
return nil, false
}
// delete 删除value
func (e *entry) delete() (value interface{}, ok bool) {
for {
// 加载指针(原子操作)
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged { // p==nil或p==expunged,则删除失败
return nil, false
}
// 将p指向nil
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}
整体流程:
// Range 回调方式依次遍历k-v
func (m *Map) Range(f func(key, value interface{}) bool) {
// 1.加载read,read.amended=true,则read与dirty数据不一致(dirty有新数据),则提升dirty,然后再遍历
read, _ := m.read.Load().(readOnly)
if read.amended {
// 加锁,准备操作dirty
m.mu.Lock()
// 双检查机制,再次加载read
read, _ = m.read.Load().(readOnly)
// read与dirty数据不一致(dirty有新数据),提升dirty为read,重置dirty和miss计数器
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
// 解锁,不再操作dirty
m.mu.Unlock()
}
// 2.回调方式遍历read
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
整体流程: