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
已经逃逸到堆上面去了。
文中开头提到的两个主要使用场景的原因主要使用的以下技术: