Go sync.Map 包教包会

4,379 阅读13分钟

一、map 与 sync.Map

1.1、map 并发

在并发情况下,map 的并发读是线程安全的,但是并发读写是线程不安全。

func main() {
   m := make(map[int]int)
   go func() {
      for {
         _ = m[1] // 读
         //m[3] = 3 // 写
      }
   }()
   go func() {
      for {
         m[2] = 2 // 写
         // _ = m[4] // 读
      }
   }()
   select {} // 维持主goroutine
}

因为 map 内部会设置读写标志位,如果识别到读写并发,就会报错(不论是否真的有并发):
fatal error: concurrent map read and map write

1.2、mutex/RWMutext+map 并发

解决线程不安全的方式也很简单:使用锁。

type LockMap struct {
   sync.RWMutex // 或 sync.Mutex
   Map map[int]int
}
func main() {
   m := LockMap{Map: make(map[int]int)}
   go func() {
      for {
         m.RLock()
         v := m.Map[2] // 读
         m.RUnlock()
      }
   }()
   go func() {
      for i := 1; i > 0; i++ {
         m.Lock()
         m.Map[2] = i // 写
         m.Unlock()
      }
   }()
   select {}
}

1.3、sync.Map 并发

官方为并发的场景提供了线程安全的 map,即 sync.Map。操作上直接使用其对外提供的 sync.Map 方法即可,不需要主动控制锁。相对来说,代码会比较干净。

func main() {
   m := sync.Map{}
   go func() {
      for i := 0; i < 10000; i++ {
         m.Store(i, i)  // 写
      }
   }()
   go func() {
      for i := 10000; i > 0; i-- {
         _, _ = m.Load(i) // 读
      }
   }()
   select {}
}

注意的是,sync.Map 不需要初始化,声明即有零值,就可以使用。

1.4、sync.Map 的操作方法

1、删除操作
func (m *Map) Delete(key interface{})
2、读操作
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
3、读取或写入
存在指定的 key 则读取,否则写入。actual为存在的 value 或新写入的 value,loaded 读操作返回true,写操作返回false。
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
4、写操作
func (m *Map) Store(key, value interface{})
5、遍历
func (m *Map) Range(f func(key, value interface{}) bool)

二、sync.Map源码分析

sync.Map 的源码(go1.14)非常少,不到400行(还包含好多注释)。而且也不涉及系统/编译之类的,就只是在 map 的基础上进行了一次封装,以支持高性能的并发读写。所以来说,源码还是比较容易阅读的,但是对于看不下去的同学,可以直接转到第三章。另外,配合第四章的行为分析,也更容易理解源码。

2.1、数据结构

type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}
type readOnly struct {
    m  map[interface{}]*entry
    amended bool   // true if the dirty map contains some key not in m.
}
type entry struct {
    p unsafe.Pointer // *interface{}
}
var expunged = unsafe.Pointer(new(interface{}))
  • mu:Mutex 锁,操作 dirty map 的时候需要加锁。
  • read:存放只读数据。因为是 atomic.Value 类型,只读,所以并发是安全的。实际存的是 readOnly 的数据结构,需要用 interface{} 进行转换。
  • amended:标记dirty和readOnly.m中的数据是否一致,不一致时为true。
  • misses:计数作用。每次读取操作时,若 read 中读失败,则计数+1。
  • dirty:存放读写数据/全量数据,就是一个原生 map,操作时需要加锁。
  • entry:readOnly.m 和 dirty 中 map 存储的值类型是 *entry,entry 中是一个指针,指向真正 map->value 的地址,从而避免了 value 数据多份存储时的空间浪费。

1、本质上,sync.Map 就是由两个 map 组成,一个存储只读数据,一个存读写数据。下文将会以 read map(即readOnly.m) 和 dirty map(即dirty) 两个名称进行阐述。

2、expunged 是占位符/哨兵值,初始化的时候随机赋值(就是一个地址值),用于标明 read map 中的 value 是否被删除。但是,删除的时候只是先将 value 置为 nil,再后续转为 expunged 值。

2.2、增/改 Store

func (m *Map) Store(key, value interface{}) {
   // 1.如果 read 中存在,并且没有被标记删除,则尝试更新
   read, _ := m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok && e.tryStore(&value) {
      return
   }

   // 如果 read 不存在该 key 或者该 key 已经被标记删除
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly)
   // 加锁后再读一次 map,因为之前的操作都是非原子性的
   if e, ok := read.m[key]; ok {
      // 2.如果 entry 被标记 expunge,则表明 dirty 没有 key,可添加到 dirty中,并更新 entry。
      if e.unexpungeLocked() {
         m.dirty[key] = e   // 加入 dirty 
      }
      e.storeLocked(&value)   // 更新 value 值
   } else if e, ok := m.dirty[key]; ok { // 3. dirty 中存在该 key
      e.storeLocked(&value)
   } else {  // 4. read 和 dirty都没有,为新增操作
      if !read.amended {  // read.amended=false 时,数据均在 read.m中,dirty=nil
         // 将 read 中未删除的数据加入到 dirty 中
         m.dirtyLocked()
         // 标记 amended=true,表示 read 与 dirty 不相同
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      m.dirty[key] = newEntry(value)
   }
   m.mu.Unlock()
}

1、read map 中若存在该 key,且没有被标记为删除状态,则直接进行 store。
注意的是,即便 m.dirty 中也有该 key,由于都是通过指针指向,所以不需要再操作 m.dirty,其 value 也会保持最新的 entry 值。

func (e *entry) tryStore(i *interface{}) bool {
   for {
      p := atomic.LoadPointer(&e.p)
      if p == expunged {  // 已经删除
         return false
      }
      if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)){//i就是value
         return true
      }
   }
}

2、read map 中若存在该 key,但已经被标记为已删除(expunged),说明 dirty map 中肯定不存在该 key,则在 dirty map 中写入该 key-value 对。同时,用 atomic.StorePointer 操作更新 read map 中的 key-value。

if e.unexpungeLocked() {
   m.dirty[key] = e   // 加入dirty 
}

func (e *entry) storeLocked(i *interface{}) {
   atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

3、read 中不存在该 key,但 dirty 中存在,则直接更新 dirty map。

4、read 和 dirty 都不存在该 key 时,则为新增操作。
此时会通过 dirtyLocked() 函数将 read map 中未删除的元素,导入到 dirty map 中。并设置 read. amended=true。

// 将read中未删除的数据加入到dirty中
func (m *Map) dirtyLocked() {
   if m.dirty != nil {  // 不需要新建/初始化dirty
      return
   }
   read, _ := m.read.Load().(readOnly)
   m.dirty = make(map[interface{}]*entry, len(read.m))
   for k, e := range read.m {
      // tryExpungeLocked()判断了该元素是否被删除,因为read.m中是存在删除数据
      if !e.tryExpungeLocked() {
         m.dirty[k] = e
      }
   }
}

在新增的时候,在 Store-> dirtyLocked-> tryExpungeLocked 中会尝试将read map 中值为 nil 的 value 置为哨兵值。

func (e *entry) tryExpungeLocked() (isExpunged bool) {
   p := atomic.LoadPointer(&e.p)
   for p == nil {
      if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { // 将 nil 替换为 expunged
         return true
      }
      p = atomic.LoadPointer(&e.p)
   }
   return p == expunged // p 已经是 expunged
}

5、引申:函数 atomic.CompareAndSwapXXX 使用手册
在 addr==old 时,才修改 addr 的值,并返回 true,否则 *addr 的值不变,并返回 false。
这系列函数在实际应用中通常会利用一个循环实现修改。

for {
   oldValue := atomic.LoadUint64(&sharedValue)
   newValue := oldValue + XXX
   if atomic.CompareAndSwapUint64(&sharedValue, oldValue , newValue ) {
      // quit only when CompareAndSwap success, otherwise retry
      break
   }
}

2.3、删 Delete

  • 如果 read map 中不存在该 key、且 dirty map 中可能存在该 key,使用 func delete(m map[Type]Type1, key Type) 进行删除元素。
  • 如果 read map 中存在该 key,将元素的 value 值标记为 nil。若此时dirty map 也存在该 key,其 value 也变为 nil,因为两个 map 中 value 用的是同一个。
func (m *Map) Delete(key interface{}) {
   // 在read map中读取
   read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
   // 如果read map中不存在该key,dirty map中可能存在该key
   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)  // map的delete
      }
      m.mu.Unlock()
   }

   if ok {
      e.delete() // 并非map的delete,只是将value置为nil
   }
}

func (e *entry) delete() (hadValue bool) {
   for {
      p := atomic.LoadPointer(&e.p)
      if p == nil || p == expunged { // 如果p指针为空,或者被标记清除,说明已经删除
         return false
      }
      if atomic.CompareAndSwapPointer(&e.p, p, nil) { // 原子操作,将其标记为nil.
         return true
      }
   }
}

1、read map 中有两种删除标记(nil 和 expunged)
标记为 nil,说明是正常的 delete 操作,此时 dirty 中不一定存在该元素:delete key 时,若 dirty 中存在 key 则 value 也被置为nil;delete key 时,若 dirty 中不存在 key 则无操作。
标记为 expunged,说明是在 dirty 初始化的时候操作的,此时 dirty 中肯定不存在。

2、read map 中被删除的元素此时为 nil 状态,什么时候变成占位符 expunged 的值?什么时候真正的删除?
插入一个新 key 的时候,触发 dirtyLocked->tryExpungeLocked,将 nil value 标记为 expunged 值。
在下一次 promoted 的时候,read 被指向 dirty,从而使被删除的内容可以被 GC。

所以,在 sync.Map 中,一个元素被删除时,并不会马上消失。别人的一个bug:将一个连接作为 key 放进 sync.Map,于是和这个连接相关的(比如 buffer 的内存)就很难释放了。

2.4、读 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 中查找
    if !ok && read.amended {
        m.mu.Lock()
        // 非原子操作,因此在加锁后需要再判断一次
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]

        // read 中不存在则去 dirty 中找;read 中存在则 e=read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 不管是否从 dirty 中得到了数据,都会将 misses 的计数+1
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

func (e *entry) load() (value interface{}, ok bool) {
   p := atomic.LoadPointer(&e.p)
   if p == nil || p == expunged {
      return nil, false
   }
   return *(*interface{})(p), true
}

1、操作流程
先在无锁条件下,查找 read map;若有,则通过 atomic 原子操作读取数据并返回;如果搜索不到并且 dirty map 中有更新,就在加锁条件下,查找 dirty map;同时更新 misses 计数。

read 的作用是在 dirty 前优先查询,遇到相同元素的时候就可以不穿透到 dirty(相当于是无锁读缓存)。

2、read.amended=true,只是说明了有有数据不在 read map中,说明 read map 中不存在的 key 可能在 dirty map 中,但是不一定会存在。

3、misses 计数的作用:触发 promoted

  • misses:在 load 查找时,key 不在 read map 中的累计次数。
  • promoted:将 dirty map 切换为 read map(将需要锁操作的 dirty 数据,转移到只读线程安全的 read 中),原 read map 指向的内存将被 GC;利用 miss 计数和 dirty 长度的比较,控制 promoted 触发。
func (m *Map) missLocked() {
    m.misses++
    // 利用 misses 计数和 dirty 长度的比较,控制 promoted 触发
    if m.misses < len(m.dirty) {
        return
    }
    
    // 将 dirty 置给 read,因为穿透概率太大了(原子操作,耗时很小)
    m.read.Store(readOnly{m: m.dirty})  // 将 dirty 数据复制到read中去
    m.dirty = nil   // dirty 清空
    m.misses = 0    // misses 重置为0
}

promoted 的速度是很快的,因为只是切换了 read 和 dirty 的指针,不需要考虑其他影响因素。注意的是,此时read.amended已经初始化为 false 了,所以源码中从没有显式地给 amended 赋值。

相对的,如果 sync.Map 的数据量很大,dirtyLocked 就会很慢,因为他是遍历赋值的过程,并且加了锁。

那么,为什么要保证 dirtyLocked 后 dirty map 需要持有全部的数据呢?———为了下次 promoted 而准备。

2.5、遍历 Range

  • read.amended=false时,说明 dirty map 中无数据,仅遍历read map即可。
  • read.amended=true时,dirty map 中存储了全量数据,仅遍历dirty map即可。
  • 但是,实际上并不是去遍历 dirty map,而是先进行一次 promote,将 dirty 切换为 read.m,再遍历 read map。
  • 注意的是,传入参数为函数,在 range 中,调用 f(k,v) 进行数据处理。
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 {
         // promote
         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
      }
   }
}

2.6、其他说明

1、为什么 dirty 可以直接换到 read?
因为写操作只会操作 dirty map,并且新建 dirty map 的时候会将 read 中数据拷贝到 dirty map 中,所以保证了 dirty 中的数据是最新最全的(数据集是肯定包含 read 的)。

2、提升 promoted 发生在读操作的时候,是将 dirty 数据转到 read 中。

3、新建 dirty 是在新增 key 的时候进行的,会拷贝 read map 中所有未删除数据。

4、key-value键值,都是可以用 nil 作为值。nil 作为 value 时,是以地址的形式存在,所以不会和删除行为混淆。

三、适合场景 & 使用效率

3.1、官方说明

从 Go 官方文档可见,在(1)写入一次后读取多次,如缓慢增长的 map;(2)并发读写不同/不相交的键值;两种 case 下,使用 sync.Map 会更高效。

3.1、官方压测

使用 go 自带的测试文件(/usr/local/go/src/sync 位置下,map_reference_test.go 文件中定义了测试用的 mapInterface 接口,map_test.go 文件中实现了三个对象的方法测试代码,map_bench_test.go 文件中是三个对象的benchmark性能对比测试代码)进行压测
// DeepCopyMap=struct{mutex+read map}

1、读,miss大量 —— BenchmarkLoadMostlyHits

/*sync_test.DeepCopyMap-8              139111147               11.7 ns/op
/*sync_test.RWMutexMap-8               21769174                50.3 ns/op
/*sync.Map-8                           143320003               8.47 ns/op

2、读,hit大量 —— BenchmarkLoadMostlyHits

/*sync_test.DeepCopyMap-8                91466886                13.3 ns/op
/*sync_test.RWMutexMap-8                 23083020                52.4 ns/op
/*sync.Map-8                             87908635                13.6 ns/op

3、读写各一半 —— BenchmarkLoadOrStoreBalanced

/*sync_test.RWMutexMap-8             3588764               356 ns/op
/*sync.Map-8                         3582439               369 ns/op

4、LoadAndStore唯一值 —— BenchmarkLoadOrStoreUnique

/*sync_test.RWMutexMap-8               2070034               590 ns/op
/*sync.Map-8                           1787559               685 ns/op

5、遍历 —— BenchmarkRange

/*sync_test.DeepCopyMap-8                   443395              2541 ns/op
/*sync_test.RWMutexMap-8                     21390             57320 ns/op
/*sync.Map-8                                401102              2709 ns/op

6、删除 —— BenchmarkAdversarialDelete

/*sync_test.DeepCopyMap-8              6994172               184 ns/op
/*sync_test.RWMutexMap-8              14045349                76.5 ns/op
/*sync.Map-8                          19644759                59.0 ns/op

3.2、自己写的压测

说实话,官方的压测代码有部分看不懂。而且测试 case 的结果出来,都是验证了 sync.Map 性能更好。但是,实际上并不是这样....... "自己写的压测"——当然不是我写的代码,是"TonyBai"大佬写的,我感觉测试结果更加直观、对比性更好。

1、测试
压测代码在 github.com/bigwhite/ex… 中,这里贴一下运行结果:

// 写入
BenchmarkBuiltinMapStoreParalell-8          8811454             199 ns/op
BenchmarkSyncMapStoreParalell-8             3034864             379 ns/op
BenchmarkBuiltinRwMapStoreParalell-8        8888641             206 ns/op
// 查找
BenchmarkBuiltinMapLookupParalell-8         8363475             179 ns/op
BenchmarkBuiltinRwMapLookupParalell-8       2358116             56.5 ns/op
BenchmarkSyncMapLookupParalell-8            57642225            23.8 ns/op
// 删除
BenchmarkBuiltinMapDeleteParalell-8         8313638             167 ns/op
BenchmarkBuiltinRwMapDeleteParalell-8       8413243             199 ns/op
BenchmarkSyncMapDeleteParalell-8            59606284            19.3 ns/op

BuiltinMap = Mutex + map
BuiltinRwMap = RWMutex + map
SyncMap = sync.Map

2、结论
读和删两方面上,sync.Map 的性能更好。
写这方面上,sync.Map 的效率只有其他两项的一半。
综上,在读多写少的场景下,使用 sync.Map 类型明显更好。

3、引申:对于写多读少的场景,怎么办呢?毕竟锁+map效率也不高。
锁分段技术:将 map 拆解为多个子 map,并由多个锁控制并发,从而降低锁粒度,以提高效率。

3.3、结果分析

1、采用读写分离的方式,降低锁时间,提升读性能
sync.Map 中并非单纯的 map,而是有两个 map ,其中的 read map 可以认为就是一个缓存层,存储了“老”数据,可以进行无锁+原子操作。而对于新写入的元素存放于 dirty map 中,并且在一定的时机下,将 dirty 中的全量数据转入到 read map 中。所以在写入少的情景下,大部分的执行,数据都在 read map 中,结合 amended 属性查找效率高。

但是写入多的场景下,会导致 dirty map 中总有新数据,查找新数据会先后在 read map 和 dirty map 中查找,并且后者需要加锁,效率就很低。同时 dirty map 也会不断触发 promoted 为 read map,整体性能很差。

2、写入性能差
写入一定要会经过 read map,所以无论如何都比别人多一层操作;后续还要查数据情况和状态,性能开销相较更大。

另外,在新增 key 的时候,常常会伴随全量数据的复制(从read到dirty),若 map 的数据量大,效率很低。

3、删除速度快
因为只是标记删除,在 promoted 的时候才真正地从 read map 中清除已删除的元素,相当于是延迟删除。

四、实验 & 行为分析

看到"TonyBai"的文章,是从 sync.Map 实例的角度分析在操作 sync.Map 过程中,其内部数据的变化,并解读其源码逻辑。我觉得很有意思,也把类似的内容(日志法分析 sync.Map)放在本章。配合着实验看源码,更容易理解、更加形象。
具体做法就是把 sync.Map 的代码拷出来,做成自己的 package;增加 print 方法;最后不停地打印各个阶段下的 sync.Map 内部数据。

4.1、初始化

var m sync_map.Map
m.Print()
-------------
-------------
start =======>> sync.Map:
    read(amendend=false):
    dirty:
    misses:0
    expunged:0xc0000981e0
end <<======= sync.Map

声明后并非为 nil,所以就可以直接使用

4.2、写入

var m sync_map.Map
m.Store("key1", "val1")
-------------
-------------
start =======>> sync.Map:
    read(amendend=true):
    dirty:
        "key1": &sync_map.entry{p:(unsafe.Pointer)(0xc000098740)}
    misses:0
    expunged:0xc0000981e0
end <<======= sync.Map

amendend 的值变为 true,新写入的数据存在于 dirty 中。

4.3、dirty map 数据 promoted 至 read map

var m sync_map.Map
m.Store("key1", "val1")
m.Load("key")   // load任意key
-------------
-------------
start =======>> sync.Map:
    read(amendend=false):
        "key1": &sync_map.entry{p:(unsafe.Pointer)(0xc000098740)}
    dirty:
    misses:0
    expunged:0xc0000981e0
end <<======= sync.Map

因为 load 的时候,key 不存在于 read map,并可能存在于 dirty map,会触发 missLocked() 逻辑。当 misses>=len(dirty map) 则进行 promote。

4.4、数据从 read map 复制到 dirty map 中

var m sync_map.Map
m.Store("key1", "val1")
m.Store("key2", "val2")
m.Load("key2")
m.Load("key2")   // promote
m.Delete("key1") // delete
m.Store("key3", "val3") // copy to dirty
-------------
-------------
start =======>> sync.Map:
    read(amendend=true):
        "key1": &sync_map.entry{p:(unsafe.Pointer)(0xc000012200)} //delete
        "key2": &sync_map.entry{p:(unsafe.Pointer)(0xc000012780)}
    dirty:
        "key2": &sync_map.entry{p:(unsafe.Pointer)(0xc000012780)}
        "key3": &sync_map.entry{p:(unsafe.Pointer)(0xc0000127c0)}
    misses:0
    expunged:0xc000012200
end <<======= sync.Map

继续新增 key2 的时候,通过 dirtyLocked(),将 read 中未删除的数据加入到dirty map 中。而到下一次 promoted 的时候,read map中被删除的 key 才会被回收(老 read 指向的内存被 GC)。

4.5、更新

1、当数据只存在 read map 中
此时,仅更新了 read map 中的 value 值。

2、未 promoted 的时候
read map 和 dirty map 中均更新。

4.6、删除

1、当数据只存在 read map 中

var m sync_map.Map
m.Store("key1", "val1")
m.Store("key2", "val2")
m.Load("key1")
m.Load("key1")
m.Delete("key1")
m.Print()
-------------
-------------
start =======>> sync.Map:
    read(amendend=false):
        "key1": &sync_map.entry{p:(unsafe.Pointer)(nil)}
        "key2": &sync_map.entry{p:(unsafe.Pointer)(0xc000100760)}
    dirty:
    misses:0
    expunged:0xc0001001e0
end <<======= sync.Map

read map 中不会删除 key,只会将 value 置为 nil

2、当key只存在于dirty map中

var m sync_map.Map
m.Store("key1", "val1")
m.Store("key2", "val2")
m.Load("key1")
m.Load("key1")
m.Store("key3", "val3")
m.Delete("key3")
-------------
-------------
start =======>> sync.Map:
    read(amendend=true):
        "key1": &sync_map.entry{p:(unsafe.Pointer)(0xc000012760)}
        "key2": &sync_map.entry{p:(unsafe.Pointer)(0xc000012780)}
    dirty:
        "key1": &sync_map.entry{p:(unsafe.Pointer)(0xc000012760)}
        "key2": &sync_map.entry{p:(unsafe.Pointer)(0xc000012780)}
    misses:0
    expunged:0xc000012200
end <<======= sync.Map

read map 中不存在数据,所以也不用处理;dirty map 中的数据会直接用map.delete 方式进行删除。

3、当 key 同时存在于 read map 和 dirty map 中

var m sync_map.Map
m.Store("key1", "val1")
m.Store("key2", "val2")
m.Load("key2")
m.Load("key2")
m.Delete("key1")
m.Store("key3", "val3")
m.Delete("key2")
-------------
-------------
start =======>> sync.Map:
    read(amendend=true):
        "key1": &sync_map.entry{p:(unsafe.Pointer)(0xc0000981e0)}
        "key2": &sync_map.entry{p:(unsafe.Pointer)(nil)}
    dirty:
        "key3": &sync_map.entry{p:(unsafe.Pointer)(0xc0000987a0)}
        "key2": &sync_map.entry{p:(unsafe.Pointer)(nil)}
    misses:0
    expunged:0xc0000981e0
end <<======= sync.Map

read map 和 dirty map 都将 value 置为 nil。注意的是,这里没有调用dirty map 的 delete。

此时,再 promoted 后,key2 也是无法被删除的,依旧存在于 read map 中。只能等新增其他 key 时,将 key2 置为哨兵后,之后在第二次 promoted 后被删除。

参考

juejin.cn/post/684490…
mp.weixin.qq.com/s/rsDC-6paC…
mp.weixin.qq.com/s/8aufz1IzE…
mp.weixin.qq.com/s/s-JbeVsym…