GO中的sync.Map源码解析

9 阅读10分钟

Map的源码解析,还是有点绕的

主要两个字段:read和dirty

我的理解是read相当于redis,dirty相当于db: 主要涉及Load()、Store()、Delete()

读:

  1. 读都从read中读取,不加锁,很快
  2. 如果read中没有,那就从 dirty 中读取,这时就会加锁读取了
  3. 如果dirty中有值,返回值,要不要更新read呢?看misses统计数量,只有达到某个阈值misses > len(dirty),才会更新read(正常我们从dirty中读取之后会重新设置到read中,但是作者选择搞个阈值来控制,这样减少拷贝值,因为拷贝值也是比较耗时的事情)

写:

  1. 写的时候,优先更新read中的key,因为这时候不需要加锁,通过原子操作CompareAndSwap(),如果能更新成功,那就赚到了
  2. 如果read中没有key,那就开始加锁了,因为可能要操作dirty了
    • 这时候还是会先看read中有没有值,有就更新read里的数据(双重校验read,因为可能其他线程把read给更新了,这时候read中又有值了,捡漏)
  3. 如果read还是没有key,那就要看这个key在不在dirty中了,如果在dirty中,直接更新dirty中的值
  4. 如果都不在,那就是新key,插入到dirty中,read这时候并没有新key,所以read的amend要为true,表示read和dirty数据不一致

删:

  1. 删除也是先判断read中有没有该key,如果有的话,直接将该key对应的entry复制为nil,标记该key删除了(虽然该key还在read中,但是entry=nil)
  2. 如果read中没有,且read和dirty数据不一致,那就要加锁操作了
  3. 同样的双重校验,判断key在不在read中,如果在的话,同1标记该key为删除(entry=nil)
  4. 如果还是不在,那就从dirty中读取该key,如果key在dirty中存在,同样将该key的entry=nil,同时将dirty中的key真正删除(要保证dirty的数据都是未被删除的数据),同时misses+1
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    m := &MapSync{}
    k := "a"
    v := "hello"
    k1 := "b"
    v1 := "world"

    m.Store(k, v) // dirty中有k,read中没有,read的amend=true
    fmt.Printf("m=%+v\n", m)
    fmt.Println("------")

    m.Load(k) // misses=1
    m.Load(k) // misses=2, misses > len(dirty)了, 把dirty的值copy给read,read中就有值k了,dirty为nil

    // 这时候read的amend=false,表示read和dirty是一致的
    m.Store(k1, v1) // !amend条件成立,把read未删除的k同步给dirty,这时候dirty有k和k1,read只有k,amend=true

    r, _ := m.dirty[k].load()
    r1, _ := m.loadReadOnly().m[k].load() // 从read中读取的
    //r2, _ := m.loadReadOnly().m[k1].load() // 从read中读取的, read中没有,一定会报错
    r2 := m.loadReadOnly().m[k1] // 这里一定是nil
    fmt.Printf("r=%+v\n", r)     // hello
    fmt.Printf("r1=%+v\n", r1)   // hello
    fmt.Printf("r2=%+v\n", r2)   // nil

    m.Load("c") // misses=1
    m.Load("c") // misses=2
    m.Load("c") // misses=3 misses条件达到了,把dirty的值赋值给read了,这时候read就有k和k1了,dirty=nil

    r1, _ = m.loadReadOnly().m[k].load()   // 从read中读取的
    r3, _ := m.loadReadOnly().m[k1].load() // 从read中读取的
    fmt.Printf("r1=%+v\n", r1)             // hello
    fmt.Printf("r2=%+v\n", r3)             // world
}

type MapSyncInterface interface {
    Load(key any) (value any, ok bool)
    Store(key, value any) (previous any, ok bool)
    Delete(key any) (old any, ok bool)
}

// new 返回的是一个指针,说明expunged只是一个指针变量,用于标记某个元素是否被删除
var expunged = new(any)

type entry struct {
    p atomic.Pointer[any]
}

func newEntry(i any) *entry {
    e := &entry{}
    e.p.Store(&i)
    return e
}

// load 获取map中value的值
func (e *entry) load() (value any, ok bool) {
    p := e.p.Load()
    if p == nil || p == expunged {
       return nil, false
    }

    return *p, true
}

// trySwap 尝试更新read中的值
func (e *entry) trySwap(value *any) (*any, bool) {
    // 这里搞个死循环,那就是如果值没标记删除,那就是要必更新成功的节奏啊
    for {
       p := e.p.Load()
       // 如果read中的值已经是标记为删除状态,那该值也不会存在于dirty中
       // 所以不能直接更新,不然会出现read中有值,但是dirty中没有
       if p == expunged {
          return nil, false
       }
       // 直到更新成功
       if ok := e.p.CompareAndSwap(p, value); ok {
          return p, true
       }
    }
}

// delete 标记删除
func (e *entry) delete() (old any, ok bool) {
    // 循环更新
    for {
       p := e.p.Load()
       // 如果entry的值已经删除或被标记为删除了,就不存在删除了
       if p == nil || p == expunged {
          return nil, false
       }
       // 标记删除,设置为nil
       if e.p.CompareAndSwap(p, nil) {
          return *p, true
       }
    }
}

// unexpungeLocked 如果旧的值被标记为删除了,重新修改为nil
func (e *entry) unexpungeLocked() bool {
    return e.p.CompareAndSwap(expunged, nil)
}

// swapLocked 设置新值,同时把旧值返回
func (e *entry) swapLocked(value *any) *any {
    return e.p.Swap(value)
}

type readOnly struct {
    m map[any]*entry // read中存储key-value

    amend bool // 如果为true,那说明read中的数据和dirty的不一致
}

// MapSync 并发安全Map
type MapSync struct {
    mu sync.Mutex

    // 读内容,类似于redis的作用,用于快速查询数据,不需要加锁
    // 如果读中没有,那就需要加锁去dirty中查询
    // 往往会进行双重读,第一遍不加锁去读数据,如果数据不存在,再加锁去取dirty数据之前,再从read中取一次
    read atomic.Pointer[readOnly]

    // 类似于DB的作用,记录写的内容,往往都是最新最准确的数据
    // 新增的key都是写到dirty中
    dirty map[any]*entry

    // misses, 指read和dirty中数据不一致的次数
    // 如果misses数大于等于dirty的长度,则表示穿透到dirty的情况比较多,
    // 这时候需要把dirty中的数据放到read中,方便后续只需要从read中读取,减少加减锁的性能
    misses int
}

// loadReadOnly 加载readOnly
func (m *MapSync) loadReadOnly() readOnly {
    if p := m.read.Load(); p != nil {
       return *p
    }

    return readOnly{}
}

// Load 根据key查询value值
func (m *MapSync) Load(key any) (value any, ok bool) {
    // 读取read
    read := m.loadReadOnly()
    // 先从read中获取key是否存在
    e, ok := read.m[key]
    // 如果key不存在read中,且read和dirty中的数据不一致了,再去dirty中读取
    // 不然如果read和dirty一致,read中没有,dirty一样没有,也没必要去dirty中读取了
    if !ok && read.amend {
       // 准备从dirty中读取,先锁住
       m.mu.Lock()

       // 双重校验
       read = m.loadReadOnly()
       e, ok = read.m[key]
       if !ok && read.amend {
          // 双重校验还是没有,且read和dirty不一样,那就从dirty中读取
          e, ok = m.dirty[key]
          m.missLocked()
       }
       m.mu.Unlock()
    }

    // 最后检查一次,不管是从read中,还是从dirty中,如果能读到,ok都有值
    if !ok {
       return nil, false
    }

    // 返回map中value的值
    return e.load()
}

// Store 更新/插入新的值
func (m *MapSync) Store(key, value any) (previous any, loaded bool) {
    // 先从read中查询是否存在该key
    read := m.loadReadOnly()
    if e, ok := read.m[key]; ok {
       // read中有数据,那就尝试在read中更新该值,
       // 为啥尝试在read中更新?通过原子操作减少加锁带来的消耗
       // 因为read中和dirty中指向的底层是一样,read中更新了,dirty中的值也会相应更新,如果dirty有该key的话
       if v, ok := e.trySwap(&value); ok {
          // 旧值是nil,说明旧值被删除了
          // 这里其实已经更新成功了,只是旧值被删除了,这里返回nil和无法获取旧值
          if v == nil {
             return nil, false
          }
          // 返回旧值的值和标识旧值存在
          return *v, true
       }
    }

    // 如果read中没有数据,那就需要加锁写入了
    m.mu.Lock()

    // 双重校验
    read = m.loadReadOnly()
    if e, ok := read.m[key]; ok {
       if e.unexpungeLocked() {
          // unexpungeLocked方法会把expunged状态变更为nil状态,如果进入该分支,说明是被标记为删除状态,那说明发生了read中的未被删除的值重新写到了dirty中,且该key就不会存在于dirty中
          // 那就需要把该key,写入到dirty中, 因为下面会更新该key为非删除状态,保证dirty中一定有read中未被删除的值
          m.dirty[key] = e
       }
       // 把read中的值更新为新值,这一步和上面的trySwap不同
       // 上面的是没加锁的,所以需要不断重试更新
       // 这里前面已经加锁了,所以只有一个线程在处理,直接更新值即可
       // 到这一步其实就更新成功了,这个v != nil的判断只是判断返回旧值有没有值
       // 这一步更新成功,对应dirty中e的值也成功了,因为e的底层是一样的
       if v := e.swapLocked(&value); v != nil {
          loaded = true
          previous = *v
       }
    } else if e, ok := m.dirty[key]; ok { // read中没有值,如果dirty中有,那就更新dirty的值
       // 什么情况会进入这个方法,那就是插入之后接着更新的情况,这时候read是没有值的
       // 下面这个方法是比成功的,因为加锁了,v != nil判断只是判断返回旧值有没有值
       if v := e.swapLocked(&value); v != nil {
          loaded = true
          previous = *v
       }
    } else { // 如果read 和 dirty中都没有,那就是新的key插入场景
       // 如果read和dirty中是一致的,那就需要更新read的amend值为true,标记为不一致
       // 什么情况下是一致的呢?
       // 1、初始化的时候,第一次插入数据的时候,amend=false
       // 2、misses数量超过了阈值,重新把dirty赋值给read,这时候amend是false
       if !read.amend {
          // 同时如果read中有未删除的值,也同步给dirty
          m.dirtyLocked()
          // 因为只写到dirty,不写read,所以两者一定是不一致的,amend=true
          m.read.Store(&readOnly{m: read.m, amend: true})
       }
       // 直接写到dirty中,这时read中是没有该key的,因为新插入的key都是在dirty中
       m.dirty[key] = newEntry(value)
    }

    m.mu.Unlock()

    return previous, loaded
}

// Delete 删除某个key
// 标记删除法
func (m *MapSync) Delete(key any) (value any, loaded bool) {
    // 从read总读取该值
    read := m.loadReadOnly()
    e, ok := read.m[key]
    if !ok && read.amend {
       m.mu.Lock()
       read = m.loadReadOnly()
       e, ok = read.m[key]
       if !ok && read.amend {
          // 从dirty获取
          e, ok = m.dirty[key]
          // 直接删除,这个方法,如果key不存在,并不会真正删除
          // 这个方法是真正删除,因为要保证dirty的数据都是未被删除的正常的数据
          delete(m.dirty, key)
          // 记一次穿透
          m.missLocked()
       }
       m.mu.Unlock()
    }

    if ok {
       return e.delete() // 赋值为nil,表示删除了
    }

    return nil, false
}

// missLocked 对misses计数,超过一定阈值后,把dirty的值赋值到read中
func (m *MapSync) missLocked() {
    m.misses++
    // 这里为啥是dirty长度呢?
    // 因为dirty代表写入的数据,等于dirty长度,相当于允许每个新key都穿透一次,如果超过了,那就说明穿透概率很高了
    if m.misses < len(m.dirty) {
       return
    }

    // 超过阈值了, 把dirty给到read,把dirty设置为nil
    // 为啥要把dirty给read,因为进入到这个方法,就是read中没有,才到dirty中的,都已经穿透了
    // 所以就把read的换成dirty的,这样就减少穿透了
    m.read.Store(&readOnly{m: m.dirty})
    // 为啥要把dirty设置为nil呢?
    // 我的理解是,既然把dirty给到read了,那大部分的读在read中就能获取到了,就不需要到dirty中查询了
    // 对于新插入的key,是写到dirty中的,但是写入dirty之前,会把read中的值同步回dirty
    m.dirty = nil
    m.misses = 0
}

func (m *MapSync) dirtyLocked() {
    // 如果dirty不为空,直接返回
    if m.dirty != nil {
       return
    }

    // 什么情况下dirty是nil?
    // 1、初始化的时候,第一次插入数据的时候,dirty是nil
    // 2、misses数量超过了阈值,重新把dirty赋值给read,这时候amend是false,dirty是nil
    read := m.loadReadOnly()

    // 把read中位nil的标记为已删除的同时,把未删除的数据赋值给dirty
    m.dirty = make(map[any]*entry, len(read.m))
    for k, e := range read.m {
       if !e.tryExpungeLocked() {
          // 能不能不同步read的数据给dirty呢?
          // 我理解是不能的,因为如果不同步read的数据给dirty,后续misses超过了,dirty直接覆盖read的值
          // dirty变为nil,那之前read中的那部分数据是不是就丢了?
          m.dirty[k] = e
       }
    }
}

// tryExpungeLocked 标记为删除
// delete方法只是把entry设置为nil
// 这个方法就是把标记为nil的entry标记为expunged
func (e *entry) tryExpungeLocked() bool {
    p := e.p.Load()
    // 不断循环,直到标记成功为止
    // 这个方法就是把标记为nil的entry标记为expunged,所以一定是为nil的才会进入循环处理
    for p == nil {
       // 标记为删除
       if e.p.CompareAndSwap(nil, expunged) {
          return true
       }
       p = e.p.Load()
    }

    // 如果p不为nil,就不会进入上面的循环,那就要判断是否已被标记为删除
    return p == expunged
}