sync.map

map 在并发下的问题

go 里面提供了 map 这种类型让我们可以存储键值对数据,但是如果我们在并发的情况下使用 map 的话,就会发现它是不支持并发地进行读写的(会报错)。在这种情况下,我们可以使用 sync.Mutex 来保证并发安全,但是这样会导致我们在读写的时候,都需要加锁,这样就会导致性能的下降。

我们知道,Golang 原生的 map不支持并发的,如果你硬要用多个 goroutine 并发读写 map,会得到 panic。

if h.flags&hashWriting != 0 {
    fatal("concurrent map read and map write")
}

例如:

var m = make(map[int]int)

// 往 map 写 key 的协程
go func() {
   // 往 map 写入数据
    for i := 0; i < 10000; i++ {
        m[i] = i
    }
}()

// 从 map 读取 key 的协程
go func() {
   // 从 map 读取数据
    for i := 10000; i > 0; i-- {
        _ = m[i]
    }
}()

// 等待两个协程执行完毕 维持主goroutine
time.Sleep(time.Second)

执行这段程序,你会得到:fatal error: concurrent map read and map write

对此常见的解决方案有两种

  • 使用 Go 1.9 之后引入的 sync.Map 实现,针对读多写少的场景做了优化;
  • 原生 map 加上一把锁(Mutex 或 RWMutex)。

原生 map 如何识别并发

原理很简单:map 内置了标志位,语义是当前有一个goroutine正在写入,

hashWriting  = 4 // a goroutine is writing to the map

使用使用 sync.Mutex/sync.RWMutex 保证并发安全

type SafeMap struct {
 	data map[interface{}]interface{}
 	sync.Mutex/sync.RWMutex
}

func (sa *SafeMap) Set(key interface{},value interface{}){
	sa.Lock()
	defer sa.UnLock()
	sa.data[key]=value
}

func (sa *SafeMap) Get(key interface{})(value interface{}){
	sa.Lock()
	defer sa.UnLock()
	value,ok := sa.data[key]
	if !ok{
		value=""
	}
	return 
}

  • 多个协程可以同时使用 RLock 而不需要等待,这是读锁

  • 只有一个协程可以使用 Lock,这是写锁,有写锁的时候,其他协程不能读也不能写

  • 写锁释放之后,其他协程才能获取到锁(读锁或者写锁)

也就是说,使用 sync.RWMutex 的时候,读操作是可以并发执行的,但是写操作是互斥的

有了读写锁为什么还要有 sync.Map?

  • 使用 sync.Mutex,但是这样的话,读写都是互斥的,性能不好。
  • 使用 sync.RWMutex,可以并发读,但是写的时候是互斥的,性能相对 sync.Mutex 要好一些。

sync.Map

设计

sync.Map 在锁的基础上做了进一步优化,在一些场景下使用原子操作来保证并发安全,性能更好。因为原子操作直接有操作系统完成。

数据结构

// sync.Map
type Map struct {
   // 互斥锁
   mu     sync.Mutex
   // 只读 map,用于读操作
   read   atomic.Pointer[readOnly]
   // dirty map,写入操作会先写入 dirty map
   dirty  map[any]*entry
   // 记录需要从 dirty map 中读取 key 的次数。
   // 也就是没有在 read map 中找到 key 的次数。
   misses int
}
// readOnly 是一个只读的 map
type readOnly struct {
   m       map[any]*entry // dirty map 中的 key 的一份快照 与dirty 中的value是同一块内存
   amended bool // 记录是否在 dirty map 中有部分 read map 中不存在的 key
}

// 实际存储值的结构体。
// p 有三种状态:nil, expunged, 正常状态。
type entry struct {
   p atomic.Pointer[any]
}

  • mu:它有一个 mu 互斥锁,用于保护 dirty map。
  • read map 是一个只读的 map,但不是我们在其他地方说的只读。它的只读的含义是,它的 key 是不能增加或者删除的。但是 value 是可以修改的
  • dirty map 是一个可读写的 map,新增 key 的时候会写入 dirty map。dirty mapd的数据总是比read map的数据要新。
  • misses 是一个 int 类型的变量,用于记录 read map 中没有找到 key 的次数。当 misses 达到一定的值的时候,会将 dirty map 中的 key 同步到 read map 中。
  • readOnly 和 dirty map 存储的信息都是entry指针。好处
    • 1.减少空间浪费
    • 2.修改值,可以改变指针指向,两处都可以生效。

在这里插入图片描述

dirty map 和 read map 之间的转换

写入新的 key 的时候,其实是写入到 dirty 中的,那什么时候会将 key 写入到 read 中呢? 准确来说,sync.Map 是不会往 read map 中写入 key 的,但是可以使用 dirty map 来覆盖 read map

dirty map 转换为 read map

  • missess 的次数达到了 len(dirty) 的时候。这意味着,很多次在 read map 中都找不到 key,这种情况下是需要加锁才能再从 dirty map 中查找的。
  • 使用 Range 遍历的时候,如果发现 dirty map 中有些 keyread map 中没有,那么就会将 dirty map 转换为 read map。然后遍历的时候就遍历一下 read map 就可以了。

read map 转换为 dirty map

  • dirty mapnil 的情况下,需要往 dirty map 中增加新的 key
  • read map 转换为 dirty map 的时候,会将 read map 中正常的 key 复制到 dirty map 中。
    但是这个操作完了之后,read map 中的那些被删除的 key 占用的空间是还没有被释放的。
    那什么时候释放呢?那就是上面说的 dirty map 转换为 read map 的时候。

sync.Map 中 entry 的状态

在这里插入图片描述

在 sync.Map 中,read map 和 dirty map 中相同 key 的 entry 都指向了相同的内容(共享的)。
这样一来,我们就不需要维护两份相同的 value 了,这一方面减少了内存使用的同时,也可以保证同一个 key 的数据在 read 和 dirty 中看到都是一致的。
因为我们可以通过原子操作来保证对 entry 的修改是安全的(但是增加 key 依然是需要加锁的)。
entry 的状态有三种:

nil:被删除了,read map 和 dirty map 都有这个 key。
expunged:被删除了,但是 dirty map 中没有这个 key。
正常状态:可以被正常读取。

  • key 被删除
  • dirty map 为 nil 的时候,需要写入新的 key,read 中被删除的 key 状态会由 nil 修改为 expunged
  • 被删除的 key,重新写入
  • read 中被删除的 key(dirty map 中不存在的),在再次写入的时候会发生

注意:expunged 和正常状态之间不能直接转换,expunged 的 key 需要写入的话,需要先修改其状态为 nil。正常状态被删除之后先转换为 nil,然后在创建新的 map 的时候才会转换为正常状态。也就是 1->2 和 4->3 这两种转换)

不存在由正常状态转换为 expunged 或者由 expunged 转换为正常状态的情况。

sync.Map源码实现

先白话文说下大概逻辑。让下文看的更快。(大概只有是这样流程就好) 写:直写。 读:先读read,没有再读dirty。在这里插入图片描述

查询

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 通过原子操作获取只读 map
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    
    // 如果read没有,并且dirty有新数据,那么去dirty中查找
    if !ok && read.amended {
        m.mu.Lock()
        // 双重检查(原因是前文的if判断和加锁非原子的,害怕这中间发生故事)
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        
        // 如果read中还是不存在,并且dirty中有新数据
        if !ok && read.amended {
            e, ok = m.dirty[key] // 从 dirty map 中获取
            // m计数+1
            m.missLocked()
        }
        
        m.mu.Unlock()
    }
    
    if !ok {
        return nil, false
    }
    return e.load()
}

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    
    // 将dirty置给read (原子操作,耗时很小)
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

为什么dirty置为nil?
猜测:

  • 一方面是当read完全等于dirty的时候,读的话read没有就是没有了,即使穿透也是一样的结果,所以存的没啥用。
  • 另一方是当存的时候,如果元素比较多,影响插入效率。

func (m *Map) Delete(key interface{}) {
    // 读出read,断言为readOnly类型
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    // 如果read中没有,并且dirty中有新元素,那么就去dirty中去找。这里用到了amended,当read与dirty不同时为true,说明dirty中有read没有的数据。
    
    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)
        }
        m.mu.Unlock()
    }
    
    if ok {
    // 如果read中存在该key,则将该value 赋值nil(采用标记的方式删除!)
        e.delete()
    }
}

func (e *entry) delete() (hadValue bool) {
    for {
    	// 再次再一把数据的指针
        p := atomic.LoadPointer(&e.p)
        if p == nil || p == expunged {
            return false
        }
        
        // 原子操作
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return true
        }
    }
}

存在于 read map 中,设置 entry 指针为 nil,但是不会删除 read map 中的 key;
只存在于 dirty map 中,则直接删除。这种情况下,会删除 dirty map 中的 key。

增(改)

func (m *Map) Store(key, value interface{}) {
    // 如果m.read存在这个key,并且没有被标记删除,则尝试更新。
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    
    // 如果read不存在或者已经被标记删除
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok { // read 存在该key
    // 如果entry被标记expunge,则表明dirty没有key,可添加入dirty,并更新entry。
        if e.unexpungeLocked() { 
            // 加入dirty中,这儿是指针
            m.dirty[key] = e
        }
        // 更新value值
        e.storeLocked(&value) 
        
    } else if e, ok := m.dirty[key]; ok { // dirty 存在该key,更新
        e.storeLocked(&value)
        
    } else { // read 和 dirty都没有
        // 如果read与dirty相同,则触发一次dirty刷新(因为当read重置的时候,dirty已置为nil了)
        if !read.amended { 
            // 将read中未删除的数据加入到dirty中
            m.dirtyLocked() 
            // amended标记为read与dirty不相同,因为后面即将加入新数据。
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) 
    }
    m.mu.Unlock()
}

// 将read中未删除的数据加入到dirty中
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }
    
    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    
    // 遍历read。
    for k, e := range read.m {
        // 通过此次操作,dirty中的元素都是未被删除的,可见标记为expunged的元素不在dirty中!!!
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

// 判断entry是否被标记删除,并且将标记为nil的entry更新标记为expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    
    for p == nil {
        // 将已经删除标记为nil的数据标记为expunged
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

// 对entry尝试更新 (原子cas操作)
func (e *entry) tryStore(i *interface{}) bool {
    p := atomic.LoadPointer(&e.p)
    if p == expunged {
        return false
    }
    for {
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
        if p == expunged {
            return false
        }
    }
}

// read里 将标记为expunge的更新为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值