目录

Go 源码之 sync.Map

一、简介

  • sync.Map 是并发安全的,内部采用读(read)写(dirty)分离,常用于 多读少写 场景

  • 不适用存储大量数据,因为最坏情况下,sync.Map 的内存会翻倍

  • 设计理念

    • 读写分离

      read:只读结构,采用原子操作,无须加锁,提高性能

      dirty:普通map,读写都加锁

      misses:从 dirty 中读取的次数,misses>=Len(dirty) dirty 升级为 read (直接复制)

      读: read 中读(原子操作,无锁),不存在,则上锁从 dirty 中读,且 misses++,misses>=Len(dirty) 则 升级为 read (复制)

      写:上锁写入 dirty

    • 延迟删除

      删除只是为 value 打一个标记,在 dirty map 提升时才执行真正的删除

    • 降低锁的粒度

      使用只读结构 read,原子操作,降低锁的粒度

二、源码

(一)结构

// 安全读写 map
type Map struct {

    mu Mutex                                             // 当写read map 或读写dirty map时 需要上锁
    read atomic.Value                          // readOnly 只读结构,,原子操作,包含了map中的一部分key:value,另外在dirty字段中
    dirty map[any]*entry                     // dirty 字段包括读写都需要加锁的字段,会升级为read字段

    misses int             // 每次从 dirty 查找时 misses+1 当 misses 达到diry map len时,dirty被提升为read 并且重新分配dirty
}
type readOnly struct {                                         // read 结构
    m       map[any]*entry                                   // 存储实际元素
    amended bool                                                         // true:表 dirty 有 read 无,可快速判断 dirty 是否存在元素
}
type entry struct {
    p unsafe.Pointer // 指向元素的地址  1. nil:正常;2. expunged:被删除;3. 指向实际元素地址
}

(二)读 Load

  • read 中存在:直接返回

  • read 中不存在:

    • amended =false,则返回 false

    • amended = true,表示 dirty 中存在 read 没有的数据,加锁,从 dirty 中读取;

      misses++,当 misses== len(dirty):将 dirty 数据赋值给 read ,并且 read 内的 amended=false,清空 dirty

func (m *Map) Load(key any) (value any, ok bool) {
  if read  中存在直接返回 {
    return value,ok
  }

  if read  中不存在 {
    if read.amended ==false{
      return nil,false
    }
    上锁
     dirty 读取  valueok 
    misses++
    if  misses== len(dirty) {
      read=dirty
      amended=false
      dirty=nil
    }
    解锁
    return value,ok
  }
}
// 读取
func (m *Map) Load(key any) (value any, ok bool) {
  // 原子操作,先从read结构中读取
    read, _ := m.read.Load().(readOnly)
  // 读取元素
    e, ok := read.m[key]
  // 元素在read结构中不存在,但是 amended=true,表示在dirty中存在
    if !ok && read.amended {
    // 存在dirty,加锁读取
        m.mu.Lock()
    // double check,避免在加锁的时候dirty map提升为read map
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended { // read 不存在,但是amended=true表示在 dirty 中存在
            e, ok = m.dirty[key] // 从dirty中读取
            m.missLocked() // misses+1 && 判断是否需要提升dirty 为 read
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
  // 返回结果
    return e.load()
}

dirty升级为read

func (m *Map) missLocked() {
    m.misses++ 

    if m.misses < len(m.dirty) {
        return
    }
  // 如果misses 大于等于 dirty的长度;则提升 dirty 为read
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

(三)写 Store

  • read 中存在 key,并且不是删除状态,则直接原子操作更新

  • read 中不存在 key 或 该 key 不可直接更新:

    • 上锁

    • read 中 存在 key:则同时更新 read 和 dirty

    • read 中 不存在 key,但是存在 dirty:则更新 dirty

    • 全新的 key,不存在 read 和 dirty:

      • 首先判断如果 read.amended == false,则改为 true,并且判断 dirty==nil 时,将 read 加载到 dirty
      • 最后 更新 dirty
    • 解锁

// Store sets the value for a key.
func (m *Map) Store(key, value any) {
 // 如果read map中存在该key 且不是删除状态  则尝试直接更改(由于修改的是entry内部的pointer,因此dirty map也可见)
 read, _ := m.read.Load().(readOnly)
 if e, ok := read.m[key]; ok && e.tryStore(&value) {
  return
 }

  // 无法在`read`中更新,则更新在 dirty
 m.mu.Lock()
 read, _ = m.read.Load().(readOnly)
 if e, ok := read.m[key]; ok { // 如果 key 在 read 中存在
  if e.unexpungeLocked() {  // 但 p == expunged,则说明 key 被删除,则 将p的状态由 expunged  更改为 nil
    m.dirty[key] = e  // b. dirty map 插入 key
  }
  e.storeLocked(&value) // 将 value 存储
 } else if e, ok := m.dirty[key]; ok {
   // 如果 key 在 read 不存在,但是 dirty 存在
  e.storeLocked(&value) // 则更新value
 } else {
   // 如果 key 在 read 和 dirty 都不存在
  if !read.amended {
   m.dirtyLocked() // 将 read 中所有的非删除的元素 加载 到 dirty
   m.read.Store(readOnly{m: read.m, amended: true}) // 将amended=true,表示 read 中没有,但是 dirty 中有
  }
   // 存入 dirty
  m.dirty[key] = newEntry(value)
 }
 m.mu.Unlock()
}

m.dirtyLocked()

是一个整体的指针交换操作。 当之前执行Load()方法且满足条件misses=len(dirty)时,会将dirty数据整体迁移到read中。

sync.Map直接将原dirty指针store给read并将dirty自身也置为nil。

因此sync.Map若想要保证在 amended=true(read和dirty中数据不一致),并且下一次发生数据迁移时(dirty → read)不会丢失数据,dirty中就必须拥有整个Map的全量数据才行。所以这里m.dirtyLocked()又会【将read中未删除的数据加入到 dirty中】。 不过dirtyLocked()是通过一个迭代实现的元素从read到dirty的复制,如果Map中元素数量很大,这个过程付出的损耗将很大,并且这个过程是在保护下的。这里迭代遍历复制的方式可能会存在性能问题

// 将 read 中所有未删除的元素复制到 dirty
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }

    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[any]*entry, len(read.m))
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

(四)删Delete

  • 存在 read 中,则直接将元素置为 nil
  • 不存在 read 但是存在 dirty 中,则直接delete删除
func (m *Map) Delete(key any) {
    m.LoadAndDelete(key)
}
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
    // 存在 dirty 中
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
      // 从 dirty 中读取
            e, ok = m.dirty[key]
            delete(m.dirty, key) // 删除

            m.missLocked() // misses++
        }
        m.mu.Unlock()
    }
    if ok { // read 中存在,则直接将元素p置为nil
        return e.delete()
    }
    return nil, false
}

常见问题

1. sync.Map结构用了原子操作read atomic.Value,那为什么不直接使用原子操作即可,还要加一个dirty

​ ==原子操作可以提高读的效率,无法提高写的效率==,相反写的效率还不如锁,这也就是为什么sync.Map推荐多读少写的原因;

假设sync.Map只用read实现,那读操作可以提高效率,但是写的性能将会很差,而且使用 dirty + 锁,可以提高read 并发写的效率

总而言之,原子操作读的效率较高,写的效率还不如加锁,加锁只是应用层面上的控制,原子操作上独占cpu操作,简单的操作会比较快,如果写操作,需要进行额外的同步和协调操作

2. sync.Map的使用场景

多读少写;原因:

  • 写 dirty 需要加锁
  • 写 read 是原子操作,性能差
  • 全新的 key 时,如果 dirty =nil 时,还会将 read 中所有数据加载到 dirty ,这个过程是加锁的,耗时

3. 为什么 sync.Map 不能被复制

​ 内置了一个sync.Mutex,不可复制

4. sync.Map是如何从dirty升级为read的

​ 每次读取 dirty 时,字段 misses++,直到misses >= Len(dirty) 时,dirty 的数据赋值给 read,然后清空 dirty

5. sync.Map的设计理念

​ 读写分离、空间换取时间、降低锁的粒度、

6. sync.Map中的readOnly中的amended的设计?

amended 表示 read 和 dirty 元素不一致,也就是 dirty 中存在 read 没有的元素

7. 什么情况下元素会同时存在 read 和 dirty,并且为什么?

当出现 dirty 升级 read 的时候,dirty 是直接全量覆盖 read 结构的,所以 dirty 的数据必须是全量的,

升级后会重置 dirty=nil,直到有新元素插入才会将 read 结构复制到 dirty

之所以 dirty 的数据必须是完整的:当修改元素时,首先会先 CAS 修改 read,不成功再加锁修改 dirty,不知道这样子做的目的是什么

8. sync.Map 最坏情况下内存翻倍

为了能将 dirty 中的数据快速提升为 readonly,dirty 中包含了 readonly 中的全部数据,意味着最坏情况下内存占用翻倍,

如果存储了大量数据的话,需要关注这点