介绍
sync.Map是go官方实现的一个thread safe的map, 适合读多写少的场景
数据结构
type Map struct {
mu Mutex // 用于read map无法命中的情况下, 回去操作dirty map, 此时会加锁
read atomic.Value // read map, 特点是这个map的操作是CAS lock free的
// dirty map 初始为 nil, 当 read map 在操作无法命中的时候,
// 回去操作 dirty map, 特点是要 lock
dirty map[interface{}]*entry
// 每当访问map时, read map 无法命中,
// 会递增 misses, misses = len(dirty) 时, 会进行提升操作
misses int
}
// read 中的 atomic.Value 实际保存的是 readOnly
type readOnly struct {
m map[interface{}]*entry // 存放 readonly map, 初始时为nil;
// amended 初始时为false, read map 和 dirty map 都为nil,
// 运行过程中会不断改变状态:
// 1. dirty提升为 read map 的时候, amended 会设置为 false, 表示 dirty 当前没有数据, 当 read.amended == false 时:
// Load 查找不到k时, 不会从dirty找
// Stroe 写入不存在的kv时, 将read中的元素拷贝到dirty, 并设置 read.amended = true
// Delete 删除不存在的k时, 不会从dirty删除
//
// 2. dirty对read map进行copy后, 会将amended设置为 true
amended bool
}
var expunged = unsafe.Pointer(new(interface{}))
type entry struct {
p unsafe.Pointer // *interface{}
}
Load操作
Load的具体实现如下
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// !ok说明read.m中没有, 如果read.amended == true,
// 说明存在于dirty中, lock住从dirty中查找
if !ok && read.amended {
m.mu.Lock()
// double-check, 原因在于!ok && read.amended不是原子的, 并发运行
// 过程中, 另一个线程的访问可能会将dirty提升为read.m, 提升后数据会在read.m中, 同时
// read.amended会设置为false, 因此需要double-check一次, 如果read.m中有数据直接返回
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 计算miss次数, 如果达到miss上限则提升read为dirty
m.missLocked()
}
m.mu.Unlock()
}
// here, 说明没有数据
if !ok {
return nil, false
}
return e.load()
}
Load的函数实现还是非常简洁和清晰的, 里面都两个需要注意的地方
- double-check
m.missLocked()
1. double-check
double-check技术是lock free编程中常用的手法, 更详细的内容可以参考 Wiki: Double-checked locking
在Load这段代码中, double-check是必须的, 原因是并发执行下, dirty 会被提升为 read (发生在而在 !ok&&read.amended
代码之间), 从而在dirty中访问不到, 但read中访问到。
2. m.missLocked()
m.missLocked()
方法算是sync.Map高效执行的原因之一, 该调用做了如下的工作:
// locked during execution
func (m *Map) missLocked() {
// 递增 misses
m.misses++
// 当misses次数小于len(m.dirty)时, 不做任何工作
if m.misses < len(m.dirty) {
return
}
// 当misses次数大于len(m.dirty)时, 提升dirty map为read map,
// 同时隐式的amended是false
m.read.Store(readOnly{m: m.dirty})
// dirty设置为nil
m.dirty = nil
// miss计数设置为0
m.misses = 0
}
例如现在read.m和m.dirty情况如下所示
现在执行如下操作
操作 | misses | read.m | misses < len(dirty) |
---|---|---|---|
初始状态 | 0 | nil | true |
Load k1 | 1 | nil | true |
Load k2 | 2 | nil | true |
Load k3 | 3 | nil | true |
接着 Load k4, 此时 misses == len(dirty), 因此 dirty会 上升为 read
那么很容易想到,接下来如果继续访问上面的key,都会在read.m中命中
load 值
确定了entry后, 调用e.load() 返回真正的 value
// 实现的atomic.Value Load, 对应entry
func (e *entry) load() (value interface{}, ok bool) {
// 从atomic.Value中加载出对应的指针
p := atomic.LoadPointer(&e.p)
// 这里会在执行一次检查, 为什么呢?
// 因为在Load()方法中, 仅仅是确定了 key对应的entry在哪里,
// 如果entry在dirty中, 则会通过这个检查, 但如果在read.m中的entry,
// 根据sync.Map的设计, entry可能处于nil或expunged的状态 (表示不存在或标记为删除)
if p == nil || p == expunged {
return nil, false
}
return *(*interface{})(p), true
}
并发Load的情况
操作 | t1 | t2 | misses < len(dirty) | 获取数据的map |
---|---|---|---|---|
t1, t2 Load k1 | 执行, lock | 等待unlock | 1, true | dirty |
执行结束, unlock | 执行, lock | 2, true | dirty | |
执行结束, unlock | ||||
t2, t2 Load k2 | 等待unlock | 执行, lock | 3, false, dirty 上升为read | dirty |
执行, lock | 执行结束, unlock | read.m | ||
执行结束, unlock |
- t1 和 t2 同时并发 Load k1, t1检查完read.m后, t1抢占到Mutex, 从dirty中获取,miss + 1 < len (dirty)
- t2检查完read.m后,等待t1 unlock
- t1 unlock后, t2继续执行, 从dirty中获取, miss + 1 < len(dirty)
- t1 和 t2 同时并发 Load k2, 这次t2抢占到Mutex, 从dirty中获取, miss + 1 == len(dirty), 因此m.dirty提升为read.m
- 在t2导致dirty提升read.m之前 , t1 检查完read.m后, 并没有在read.m后检索到,此时t1等待t2 unlock
- t2 unlock后, t1继续执行, 这时进入 double-check, t1在read.m中检测到了刚刚由t2触发的提升的dirty中的数据, t1 unlock, 返回。
或者下图可以更清晰的表示这个过程
Store操作
Store的具体实现如下
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
// key存在于read.m, tryStore尝试存储新的value,
// tryStore成功直接返回
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// tryStroe失败, lock住开始继续操作
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// read.m中有对应的entry, 但被设置为expunged,
// 因此不可再read.m中使用了, 这里将entry设置为unexpunge并
// 存储到dirty
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// read.m中没找到, dirty中找到, 更新dirty中对应的value
e.storeLocked(&value)
} else {
// !read.amended 表示dirty为nil,
// 需要创建dirty并复制read.m到新的dirty
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
// 从read复制到dirty中
m.dirtyLocked()
// 将read.amended 标记为 true
m.read.Store(readOnly{m: read.m, amended: true})
}
// read.amended表示dirty不为nil, 直接将新的
// kv存储到dirty中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
Store的基本思路是:
- read.m[key] ok,则通过tryStore进行update,update成功后直接返回
- tryStore失败,key对应的value是expunged状态,进行double-check后,如果read.m[key] ok,则将read.m[key]得到的entry设置为unexpunged状态存储到dirty中
- 如果read.m[key] !ok, 但m.dirty[key] 是ok的,则直接update m.dirty[key]后返回
- 如果m.dirty[key] 也不ok, 如果!read.amended, 表示m.dirty为nil, 需要创建m.dirty, 同时将read.m中不是nil和状态不为expunged的entry复制到m.dirty
- 最后m.dirty[key] store对应的value
tryStore 的实现
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
// read.m中的entry状态为expunged, 不会去Store新的值
if p == expunged {
return false
}
// 使用CAS操作存储新的值
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
当entry不是expunged的时候,tryStore使用CAS更新value
dirtyLocked的实现
func (m *Map) dirtyLocked() {
// 仅在dirty存在时才会进行拷贝
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
// 从read复制到dirty
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
// e不是nil或unexpunged的状态下, 才会复制到dirty
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
Delete实现
Delete 的代码如下
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// double-check
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
// 从dirty删除
delete(m.dirty, key)
}
m.mu.Unlock()
}
// 从read.m中删除
if ok {
e.delete()
}
}
Delete的基本思路
- read.m[key]找到则删除
- read.m[key]没有找到并且read.amended (dirty存在), double-check后如果read.m[key]找到了, 则unlock后删除
- 否则从m.dirty中删除
delete实现
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
// p已经是删除状态
if p == nil || p == expunged {
return false
}
// 使用CAS设置p=nil
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
实际的value清除由delete实现,如果value是nil或者expunged,则不进行CAS
Range实现
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
// 只要read.amended为true, 则dirty中存在数据且数据没有提升到read
if read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
// double-check
if read.amended {
// 拷贝m.dirty
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
// 遍历并传入到user func
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
因为是thread-safe的map,因此提供了一个Range方法来提供map的遍历,Range实现思路是:
如果read.amended = true, 说明有数据存在dirty不存在read, 此时需要lock copy, 之后遍历read就是lock free的了