golang的SDK中提供线程安全的map实现sync.Map。它是针对RWMutex+map的实现方案中存在cache line的false share提出来的。主要适用于两个场景:
在这两种情况下,相比一个Mutex或者RWMutex加上普通的map,锁的竞争要少的多。那为什么呢?
type Map struct {
mu Mutex
// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Value // readOnly
// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[interface{}]*entry
// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
// An entry is a slot in the map corresponding to a particular key.
type entry struct {
// p points to the interface{} value stored for the entry.
//
// If p == nil, the entry has been deleted and m.dirty == nil.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer // *interface{}
}
Map.read包含了部分数据,读写请求优先考虑read,针对它的操作都是CAS,无锁的。
Map.dirty包含的数据是read的超集,对他的操作需要加锁。
readOnly.m表示当前read的数据,readOnly.amended表示是否有数据在dirty中。
entry保存具体数值的指针。有三种情况:
nil,表示已经删除,这个时候dirty中entry的值也是nil,因为他们是同一个entry的地址。expunged,表示数据已经擦除,entry不在dirty中。dirty中。sync.Map包含五个接口:Load、Store、LoadOrStore、Delete和Range。
这几个接口都有类似的模式:
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
ret := do_the_operation()
if ret is success {
return
}
}
m.mu.Lock()
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
ret := do_the_operation()
} else if e, ok := m.dirty[key]; ok {
ret := do_the_operation()
}
m.mu.Unlock()
利用read的CAS操作减少锁并发,同时由于并发存在获取锁之后还是有可能数据已经在read中,因此还是对read再做一次同样的操作,复用内存。如果数据仍然不在read中才会考虑操作dirty。针对数据是否在read中这几个接口的逻辑如下:
read包含了部分数据,如果key存在并而且与它对应的entry不是expunged,数据操作优先在这里进行。
Store:直接更新entry值。Load:直接返回entry值。LoadOrStore:针对entry做tryLoadOrStore操作。Delete:把entry设置成nil。当数据不在read中时,就会涉及到dirty了:
Store:entry在read中并且是expunged,则复用,同时把它修改成nil,然后把entry赋值到dirty,这样就避免了只在read不在dirty的情况。entry在dirty中,那么更新entry值。entry也不在dirty中,如果dirty是nil,则复制read中的entry值非nil的数据。然后,添加值到dirty。Load:从dirty查找,同时增加misses。如果超过一定的阀值,就会发生数据从dirty到read的迁移。LoadOrStore:流程和Store接口类似,只是返回值和对entry的处理逻辑不一样。entry有值,则返回具体值以及存在的标识。entry值为nil,设置entry为新的值并返回它和不存在标识。entry值是expunged,则返回nil和存在标志,这个是比较特殊。在LoadOrStore同时,并发的把entry从read复制到dirty,这种情况就会发生。Range接口相对简单,如果有部分数据在dirty中就会把dirty的数据提升到read中,并重置dirty。然后,遍历的是dirty数据。否则,只遍历read中的数据。这里不保证能遍历到之后添加的数据。
通过上面的逻辑我们发现read和dirty直接数据流转逻辑如下:
read到dirty:在Store和LoadOrStore的时候,如果需要保存的key既不在read也不在dirty,而且这时dirty是nil,就会把read中的nil数据变成expunged,并复制除了这份以外的数据到dirty。dirty到read:Load和LoadOrStore的时候,如果read中不存在,需要从dirty中获取数据,就会增加misses,当misses等于dirty的大小时,就会把dirty封装成readOnly,然后原子的赋值给read,并重置dirty。Range的时候,如果有数据不在read中同样会把dirty封装成readOnly,然后原子的赋值给read,并重置dirty数据。为什么需要expunged状态?
> 如果没有这个状态,更新已经删除的但是已经存在的数据就需要加锁了。
为什么newEntry的时候取的是参数interface{}的地址,这个地址不是栈上的么,会不会有问题?
> 参数i的地址被保存到map中时,变量&i已经逃逸到堆上面去了。
文中开头提到的两个主要使用场景的原因主要使用的以下技术: