Go源码解读-sync.Map的实现

简介: Go源码解读-sync.Map的实现

前言


我有一个朋友, 最近困扰于map的线程安全问题, 每次都要单独定义个结构体加锁处理, 例如以下结构体


type SafeMap struct {
  m map[string]interface{}
  mu sync.RWMutex
}


每次都要加锁解锁太麻烦, 问我有没有其他的实现方式

这不巧了吗, 官方考虑到了这种情况已经实现了sync.Map 供使用,让我们看看它是怎么实现的


正文


存储结构体

type Map struct {
  // 操作写map和miss计数器的时候加锁
  mu Mutex
  // 读map
  read atomic.Value // readOnly
  // 写map, 如果不为nil的话里面存放除已删除外的所有数据
  dirty map[interface{}]*entry
  // miss计数器, 数量>=len(dirty)的时候写map会升级为读map
  misses int
}
type readOnly struct {
  // 只读结构map
  m       map[interface{}]*entry
  // 如果写map中读map不存在的key时值为true, 为false的时候写map为nii
  amended bool 
}
type entry struct {
  // 存放值的地址, 方便后面用原子的方法进行比较和替换
  p unsafe.Pointer
}

Load 获取指定key的值


优先去读map中获取值, 如果没有并且读写map不一致, 则去读map中获取一次, 并增加一次miss计数

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
  // 获取读map
  read, _ := m.read.Load().(readOnly)
  // 判断读map里是否存在这个key
  e, ok := read.m[key]
  // 如果读map不存在这个key并且写map里存在它没有key
  if !ok && read.amended {
    // 加锁准备查写map
    m.mu.Lock()
    // 为了防止加锁过程中写map升级为读map, 这里再查一次读map
    read, _ = m.read.Load().(readOnly)
    e, ok = read.m[key]
    // 如果还是不存在key并且写map可能存在
    if !ok && read.amended {
      // 去写map里面获取这个key
      e, ok = m.dirty[key]
      // 不管查没查中都加一次miss数
      m.missLocked()
    }
    // 解锁
    m.mu.Unlock()
  }
  // 如果都不存在这个key, 返回
  if !ok {
    return nil, false
  }
  // 存在返回
  return e.load()
}
// load 获取映射的值
func (e *entry) load() (value interface{}, ok bool) {
  // 获取值的地址
  p := atomic.LoadPointer(&e.p)
  // 如果为nil/expunged, 则证明这个key被删除了, 返回nil,false
  if p == nil || p == expunged {
    return nil, false
  }
  // 正常返回
  return *(*interface{})(p), true
}
// missLocked 增加写map miss计数
func (m *Map) missLocked() {
  // miss数自增
  m.misses++
  // 如果miss数小于写map的长度, 则不做操作
  if m.misses < len(m.dirty) {
    return
  }
  // miss数 >= 写map的长度, 读map升级为写map
  m.read.Store(readOnly{m: m.dirty})
  // 读map重置为nil
  m.dirty = nil
  // miss数重置为0
  m.misses = 0
}

Store 添加/修改


读map存在key且未删除时直接修改值的地址

读map存在key且key被删除, 则写map增加key, 然后修改值的地址

读map不存在key且写map存在, 直接修改值的地址

读写map都不存在key, 写map为nil的话复制读map, 写map新增值


func (m *Map) Store(key, value interface{}) {
  // 获取读map
  read, _ := m.read.Load().(readOnly)
  // 如果读map中存在这个key 并且尝试修改值, 成功则返回
  if e, ok := read.m[key]; ok && e.tryStore(&value) {
    return
  }
  // 加锁
  m.mu.Lock()
  // 为了防止加锁过程中写map升级为读map, 这里再查一次读map
  read, _ = m.read.Load().(readOnly)
  // 如果读map中存在这个key
  if e, ok := read.m[key]; ok {
    // 判断原读map是不是已经删除这个key, 如果是改为nil, 返回true, 否则为false
    if e.unexpungeLocked() {
      // 修改写map
      m.dirty[key] = e
    }
    // 修改value值
    e.storeLocked(&value)
  } else if e, ok := m.dirty[key]; ok {
    // 如果这个key不存在读map且存在于写map, 则直接修改value值
    e.storeLocked(&value)
  } else {
    // 如果这个key读写map都不存在 且 写map为nil(升级为读map后未进行更新)
    if !read.amended {
      // 写map复制读map中除删除外的数据
      m.dirtyLocked()
      // 读map的 amended 改为true, 即写map拥有读map不存在的key
      m.read.Store(readOnly{m: read.m, amended: true})
    }
    // 写map添加key, value
    m.dirty[key] = newEntry(value)
  }
  // 解锁
  m.mu.Unlock()
}
// tryStore 尝试修改值
func (e *entry) tryStore(i *interface{}) bool {
  for {
    // 获取值的地址
    p := atomic.LoadPointer(&e.p)
    // 如果=expunged, 为已删除, 则返回false
    if p == expunged {
      return false
    }
    // 原子操作修改地址指向 i
    if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
      return true
    }
  }
}
// unexpungeLocked 如果原地址为expunged(已删除), 则修改为nil, 否则返回false
func (e *entry) unexpungeLocked() (wasExpunged bool) {
  return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// storeLocked 原子存储值的地址
func (e *entry) storeLocked(i *interface{}) {
  atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
// dirtyLocked 写map操作
func (m *Map) dirtyLocked() {
  // 如果写map不等于nil, 返回, 这块应该是必等于nil的, 只有当写map升级为读map后read.amended才为false
  if m.dirty != nil {
    return
  }
  // 获取读map
  read, _ := m.read.Load().(readOnly)
  // 写map创建map
  m.dirty = make(map[interface{}]*entry, len(read.m))
  // 循环写map写入
  for k, e := range read.m {
    // 判断值是否为已删除, 已删除的不写入写map
    if !e.tryExpungeLocked() {
      // 写map赋值
      m.dirty[k] = e
    }
  }
}
// tryExpungeLocked 尝试修改为nil为expunged
func (e *entry) tryExpungeLocked() (isExpunged bool) {
  // 获取值的地址
  p := atomic.LoadPointer(&e.p)
  // 如果等于nil的死循环修改为expunged
  for p == nil {
    // 原子操作修改原值为nil的话改为expunged
    if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
      return true
    }
    // 失败的话重新获取值
    p = atomic.LoadPointer(&e.p)
  }
  // 返回值 == 已删除
  return p == expunged
}

Delete 删除


  1. 读map有的话直接修改值的地址为nil;
  2. 读map没有并且与写map不一致时, 强删key


func (m *Map) Delete(key interface{}) {
  // 获取读map
  read, _ := m.read.Load().(readOnly)
  // 获取读map是否存在这个key
  e, ok := read.m[key]
  // 如果读map不存在这个key并且写map里存在它没有key
  if !ok && read.amended {
    // 加锁
    m.mu.Lock()
    // 为了防止加锁过程中写map升级为读map, 这里再查一次读map 
    read, _ = m.read.Load().(readOnly)
    e, ok = read.m[key]
    // 如果还是不存在key并且写map可能存在
    if !ok && read.amended {
      // 如果写map存在这个key并且没被删除, 则修改为nil
      delete(m.dirty, key)
    }
    // 解锁
    m.mu.Unlock()
  }
  // 如果读map存在
  if ok {
    // 修改为nil
    e.delete()
  }
}
// delete 删除
func (e *entry) delete() (hadValue bool) {
  for {
    // 获取值
    p := atomic.LoadPointer(&e.p)
    // 如果为nil/expunged, 则证明这个key被删除了, 返回false
    if p == nil || p == expunged {
      return false
    }
    // 修改值为nil
    if atomic.CompareAndSwapPointer(&e.p, p, nil) {
      return true
    }
  }
}


Range 循环


  1. 读写map不一致, 则写map升级为读map
  2. 循环读map进行操作
func (m *Map) Range(f func(key, value interface{}) bool) {
  // 获取读map
  read, _ := m.read.Load().(readOnly)
  // 如果写map存在读map中不存在的key
  if read.amended {
    // 解锁
    m.mu.Lock()
    // 为了防止加锁过程中写map升级为读map, 这里再查一次读map 
    read, _ = m.read.Load().(readOnly)
    // 如果写map还是存在读map中不存在的key
    if read.amended {
      // 写map升级为读map
      read = readOnly{m: m.dirty}
      m.read.Store(read)
      // 读map重置为nil
      m.dirty = nil
      // miss数重置为0
      m.misses = 0
    }
    // 解锁
    m.mu.Unlock()
  }
  // 循环读map, 读map一定为当前最全的值
  for k, e := range read.m {
    // 获取值
    v, ok := e.load()
    // key被删除则跳过
    if !ok {
      continue
    }
    // 如果循环函数返回false, 则终止循环
    if !f(k, v) {
      break
    }
  }
}
// load 获取值
func (e *entry) load() (value interface{}, ok bool) {
  // 获取值
  p := atomic.LoadPointer(&e.p)
  // 如果为nil/expunged, 则证明这个key被删除了, 返回nil, false
  if p == nil || p == expunged {
    return nil, false
  }
  // 正常返回
  return *(*interface{})(p), true
}

总结


sync.Map 是用读写分离的方式实现的, 用空间换时间, 最多不超过一倍的内存占用(如果读map=写map的话就会把写map升级成读map, 写map置空);


只适用于少量新增的方式, 或者在初始化写后少量新增, 不然写map一直升级为读map, 性能可能会拉跨;


相对的, 对于大量读和更新, 绝对比上述自己加锁的方式性能要好上很多;


思考


en方法的实现, 可能是因为并发操作导致更新比较快, 数据没有什么参考意义所以没有实现, 想自己实现的话参考 Range方法就统计值就可以了;


线程安全的map 性能瓶颈主要在加锁这块, 在大量写的情况下肯定是不能用sync.Map的, 最好的方法应该是使锁的粒度尽可能的小, 也是对map进行分组操作(这不就跟数据库优化方案一样了, 先读写分离, 再分表分库);


expunged 的设计点, 我自己尝试修改了源码删除了expunged, 发现也可以正常使用, 也不会出现其他博主说的会造成脏内存的情况, 这块还得再想想, 或者哪位大神可以解释下,


更新下第三条, expunged 的设计点我想明白了

有expunged

删除 -> 指针的值为nil, 读写map都存在;
更新 -> 指针的值为nil, 直接修改;
更新 -> 指针的值为exp, 加锁, 尝试修改exp为nil(如果之前为exp, 写map增加key), 修改nil为真正的值
新增 -> 写map为nil的话, 将所有的nil改为exp, 存放读map中不为exp的值

无expunged


删除 -> 指针的值为nil, 读写map都存在;
更新 -> 指针的值为nil; --> 这块必须要更新写map, 因为写map不存在这个key;
新增 -> 写map为nil的话, 写map存放读map中不为nil的值;


值为expunged的key是写map中不存在的, 为nil的key在写map重新赋值前是可以重复利用的; 这就是sync.map对大量读和更新特别友好的关键所在




目录
相关文章
|
1月前
|
Go
Go 语言为什么不支持并发读写 map?
Go 语言为什么不支持并发读写 map?
|
3月前
|
Go
go语言map、实现set
go语言map、实现set
|
16天前
|
存储 算法 Java
Go 通过 Map/Filter/ForEach 等流式 API 高效处理数据
Go 通过 Map/Filter/ForEach 等流式 API 高效处理数据
|
28天前
|
存储 安全 NoSQL
Go map 读写性能优化 - 分片 map
Go map 读写性能优化 - 分片 map
26 1
|
28天前
|
存储 人工智能 安全
go sync.Map 设计与实现
go sync.Map 设计与实现
17 1
|
11天前
|
存储 缓存 Go
如何检查 Go map 是否包含某个键?
【8月更文挑战第31天】
9 0
|
1月前
|
算法 安全 Go
go 语言中 map 的相关知识
go 语言中 map 的相关知识
|
14天前
|
存储 Go 容器
Go从入门到放弃之map(字典)
Go从入门到放弃之map(字典)
|
16天前
|
存储 Java 关系型数据库
听说过对 Go map 做 GC 吗?
听说过对 Go map 做 GC 吗?
|
1月前
|
设计模式 安全 Java
Go - 使用 sync.Map 来解决 map 的并发操作问题
Go - 使用 sync.Map 来解决 map 的并发操作问题
16 1