Golang:sync.Map

并发安全

  • Go自带的map并不是并发安全的。可以使用sync.Mutex或sync.RWMutex加上原生map自行构建。
  • 在1.9版本之后,Go正式加入了并发安全的字典类型sync.Map。
  • sync.Map提供了常用的键值存取操作,并保证了操作的并发安全,并且时间复杂度与map一样都是O(1)。
  • 使用锁意味着要把一些并发操作串行化,实际上是降低了程序性能,特别是在多核CPU上;所以常说,能使用原子操作就不要用锁,不过原子操作只能对一些基本的数据类型提供支持,所以很有局限性。
  • Go语言编译器不会对sync.Map的键或者值进行特殊类型检查。参数类型都是interface{}
  • sync.Map对键的类型要求和map一样,不可以是切片,字典,函数,必须是可以判等的类型。由于这些键值的实际类型只有在程序运行期间才能确定,所以Go语言编译器是无法在编译期检查,不正确的实际类型肯定会在运行时panic。
  • 所以在每次操作sync.Map时,应该显式检查键值的实际类型。更好的做法是,把针对同一个sync.Map的操作集中起来,统一编写检查代码,除此之外,把sync.Map封装在一个结构体中,往往是一个好选择
  • 我们必须保证键的类型是可比较的。可以使用reflect.TypeOf(a).Comparable()得到a变量的类型是否是可比较的。

sync.Map怎样做到了尽量避免使用锁

  • sync.Map 类似map[interface{}]interface{},但是在多个goroutine使用时不需要额外的锁或者协调,就是安全的。加载、保存、删除平均时间复杂度是O(1)。
  • sync.Map是专业的,大多数代码应该使用普通的map代替
  • sync.Map的使用场景主要有两个:一写多读的增长型缓存。当多个goroutine读、写和覆盖不相交键集。在这两种情况下,sync.Map比起使用map和锁实现的方式,减少了锁的争用。
  • sync.Map在内部使用大量原子操作,来存取键值,使用了两个原生的map作为存储介质。

只读字典和脏字典

  • 其中一个map放在read字段里,read字段是atomic.Value,往这个atomic.Value里存储的是readOnly结构体,这个结构体有个字段是map。我们称这个map是只读字典,它不会增减键,但是可以修改键的值。
  • 由于read字段是atomic.Value类型的,所以在替换只读字典时根本用不到锁。atomic.Value的Store和Load方法是原子性的
  • 只读字典在保存键值对时,还在值上封装了一层,即entry结构体。它先把值转换成unsafe.Pointer,然后放到entry的p字段中,再保存到readOnly的m字段里,这样,在修改键对应的值时,也可以使用原子操作了。unsafe.Pointer可以使用除了add之外的原子操作,比如load和store
  • dirty字段,它存储键值对的方式和read字段(readOnly)中的原生字典一致,同样是把值做转换和封装后进行存储的,称为脏字典。
  • 如果脏字典和只读字典都存在同一个键值对,那么这里的两个键指的肯定是同一个基本值,对于两个值来说也是如此?这两个字典在存储键和值时只会存入它们的某个指针,而不是基本值。
type Map struct {
	mu Mutex
	read atomic.Value
	dirty map[interface{}]*entry
	misses int
}

type readOnly struct {
	m       map[interface{}]*entry
	amended bool
}

var expunged = unsafe.Pointer(new(interface{}))

type entry struct {
	p unsafe.Pointer // *interface{}
}

func newEntry(i interface{}) *entry {
	return &entry{p: unsafe.Pointer(&i)}
}
  • sync.Map在查找指定的键所对应的值时,先从只读字典中找,先把readOnly获取到,这步是原子操作,然后看看有没有要查找的key,如果没有,并且readOnly的amended是true(表示脏字典里可能有),就加上锁,然后重新获取下readOnly。为什么要加锁后再重新获取呢,涉及到双重判断的问题,因为可能进到if代码段后,之前的状态修改了。重新判断之后,如果可能脏字典里面有,就去脏字典获取。如果还没有获取到,那么返回nil,否则再进行原子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()
}

func (e *entry) load() (value interface{}, ok bool) {
	p := atomic.LoadPointer(&e.p)
	if p == nil || p == expunged {
		return nil, false
	}
	return *(*interface{})(p), true
}

// 如果在只读字典中未命中的次数和脏字典长度相同,那么就把脏字典刷到只读字典中,然后脏字典设置为nil并重新计数
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}
  • 在存储键值对时,只要只读字典中有这个键,并且键未标记为已删除,就把新值直接存储。如果没有的话,就在锁的保护下,把键值存到脏字典。
func (m *Map) Store(key, value interface{}) {
	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() {
			// The entry was previously expunged, which implies that there is a
			// non-nil dirty map and this entry is not in it.
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		if !read.amended {
			// We're adding the first new key to the dirty map.
			// Make sure it is allocated and mark the read-only map as incomplete.
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}
  • 删除操作,先查下只读字典有没有,如果没有,并且可能脏字典有,那么就和load方法一样,最终删除脏字典中的数据。
  • 如果在只读字典中,那么就使用原子操作CAS,将空指针和要删除的值交换。
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
	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 {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		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
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值