go sync map

sync.Map相当于goroutine并发安全的Map。

sync.Map针对两种常见情况进行了优化

  • 给定key的entry只写一次,但读取多次时。比如缓冲增长
  • 当多个goroutine对不相交key集进行读写覆盖

sync.Map在第一次使用后不能被复制

过程展现

  1. 最开始写入数据只会到dirty中,因为dirty主要接受写数据。此时read amended为true

  2. 读取时从read读取,此时read并没有数据,misses记录从read读取失败的次数,当misses大于等于dirty数据量时,将dirty升级为read。

    升级也就是对dirty进行地址拷贝并且dirty清空,misses置为0

    此时read amended为false

  3. 修改元素会直接修改read。此时read amended为false

  4. 新加元素,此时会直接操作dirty。从read复制元素到dirty且添加新元素到dirty。此时read amended为true

结构

type Map struct {
  mu Mutex
  
  // read包含map中可以安全并发读取的部分内容
  // 读取read字段本身是安全的,但存储时必须持有mu
  // 存储在read中的entry可以在没有mu的情况下并发更新,但更新一个已清除的entry时,需要将该数据项复制到脏map中,并在保持mu的情况下不清除。
  // 只读数据,支持并发数据。若涉及到更新操作,则需要加锁来保证数据安全
  read atomic.Value // readOnly
  
  // dirty包含了map中需要保存mu的部分内容。为确保dirty map可以快速提升到read map,它还包括read map中所有未清除的项。
  // expunged项不会存储在dirty中。在将新值存储到dirty的该key位置之前,clean map中的expunged项必须恢复并添加到dirty中。
  // 如果dirty为nil,下一次对map的写入将通过创建clean map的副本来初始化它,并省略陈旧的元素。
  dirty map[interface{}]*entry
  
  // misses记录自从dirty提升为read以来dirty的访问(读取、删除、更新)次数
  // 一旦misses达到dirty数据量,dirty就会被提升到read(处于未修改状态),然后下一个存储会创建一个新的dirty。
  misses int
}

type readOnly struct {
  m       map[interface{}]*entry
  // 标记dirty是否储存read没有的数据
	amended bool // true if the dirty map contains some key not in m.
}

// read、dirty各自维护一套key,指向同一个value。也就是说只要修改了这个entry,对read、dirty都是可见的
// p有nil、expunged、正常 3种状态
// - nil: 代表该key-value被删除了,dirty为nil或者dirty[key]是e
// - expunged: 代表被删除,但此key只在read而没有在dirty中。出现在将read复制到dirty,复制过程中会先将read该key由nil标记为expunged,然后不将其复制到dirty
// - 正常: 存在数据
type entry struct {
	p unsafe.Pointer // *interface{}
}

Store

func (m *Map) Store(key, value interface{}) {
  // 若readMap中存在该key,则直接尝试修改(由于修改的是 entry 内部的 pointer,因此 dirty map 也可见)
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
      // entry之前是expunged,暗示有不含该entry的非nil dirty map
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		if !read.amended {
			m.dirtyLocked()
      // readOnly是结构,更新只能重新Store
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}
  1. 检查read是否存在该插入元素key,若存在且没有标记删除状态,则尝试更新read取出的entry

  2. 加锁。再次检查read是否存在元素,若read存在

    在取出entry为expunged情况下,就插入新元素到dirty,并更新read取出的entry

    1. 将从read取出的entry状态从expunged改为nil成功后(标记为删除,则说明dirty不等于nil且dirty不存在该值),将元素插入dirty
    2. 更新entry值为插入值
  3. 若read不存在该元素,但dirty存在,则直接写入更新entry

  4. 若发现read、dirty都不存在该key,则

    dirty、read一致时就复制read到dirty,然后dirty再插入新元素

    1. 若amended记录dirty不存在read不存在的元素则

      复制read到非nil dirty中,amended置为true

    2. dirty插入插入元素

Load

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		// Avoid reporting a spurious miss if m.dirty got promoted while we were
		// blocked on m.mu. (If further loads of the same key will not miss, it's
		// not worth copying the dirty map for this key.)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
  1. 检查read是否包含所需元素,若有,则通过原子操作读取数据并返回

  2. 判断dirty是否包含read中没有的数据,若存在则会加锁,先到read,然后到dirty查找数据。

    misses加一,当misses大于等于dirty数据量,则保存dirty到read,并置misses为0,dirty为nil

  3. 其他情况则找不到

LoadAndDelete

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			delete(m.dirty, key)
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if ok {
		return e.delete()
	}
	return nil, false
}

func (e *entry) delete() (value interface{}, ok bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == nil || p == expunged {
			return nil, false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return *(*interface{})(p), true
		}
	}
}
  1. 尝试从read获取值。若不存在且dirty存在read不存在值时
    1. 加锁。再从read双重锁读取,在仍然不存在且dirty存在read不存在值时,删除dirty对应值。并记录misses加1,且尝试升级dirty
  2. 若能找到,则
    1. entry若是nil或者expunged,则返回说明已经删除了
    2. 尝试CAS更新状态为nil,若更新成功,则返回。不行则一直遍历,直到成功为止

显然可知如果几乎没有什么读的情况,将导致key、value在read中无法删除。并且read中的key也不会被删除

Ref

  1. https://pkg.go.dev/sync#Map
  2. https://segmentfault.com/a/1190000040729053
  3. https://juejin.cn/post/6844903618525528077
  4. https://studygolang.com/articles/30385
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值