Go 1.9中的sync.Map提供了线程安全的map,它的优点总结如下:(网上找的)
1.空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
2.使用只读数据(read),避免读写冲突。
3.动态调整,miss次数多了之后,将dirty数据提升为read。
4.double-checking。
5.延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
6.优先从read读取、更新、删除,因为对read的读取不需要锁。
其实当我们读完它内部实现后就能很好地理解以上好处了。
先从Map的数据结构看起
type Map struct { mu Mutex read atomic.Value // readOnly dirty map[interface{}]*entry misses int }
Mutex加锁的工具,read一个只读的数据结构,所以对它的读总是线程安全的。dirty是map结构,它包含整个Map中的最新的entries,它不是线程安全的,为了保证多线程安全的情况下操作它,需要对它加锁;当dirty为空时,下一次写操作会复制read字段中未删除的数据到dirty。misses计数器,当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一, 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
其中read atomic.Value中存的是readOnly数据结构,我们来看下readOnly
type readOnly struct { m map[interface{}]*entry amended bool // true if the dirty map contains some key not in m. }
m无非存数据,amended为true的话说明,dirty数组包含一些新的entries,却存没有在readOnly中。
因为这里数据是只读的,所以读数据时候优先读read数据,若read中没有,则去dirty中读,读到则misses加一。
Map中还一个expunged指针,指向被删除的键值对。Map中的entry结构体无非存有指向value的指针
var expunged = unsafe.Pointer(new(interface{})) // An entry is a slot in the map corresponding to a particular key. type entry struct { p unsafe.Pointer // *interface{} }
既然是Map,那我们先看下如何存数据,Store(key, value interfaces{})
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() }
先看这个情况:第一次new() Map后,里面肯定没有任何数据,第一次Store传入键值对到Map中。
首先通过atomic.Value的Load方法,加载readOnly到read中,此时read中肯定没有任何数据,于是我们要从dirty中取数据,由于它不是线程安全,所以调用mutex锁工具lock,于是读dirty中,也找不到,则判断下amended是否为false(确保是不是第一次往dirty中加数据),由于readOnly是刚创建的其ammended值为false,所以此时方法调用进入m.dirtyLocked()
func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } } }
该方法创建dirty,把readOnly的(未被删除的)键值对复制到dirty中,此时readOnly为空。
func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
创建完dirty后,会创建新的readOnly赋值给read,此时amended为true;最后在dirty中添加键值对,解锁。
情况二:如果此时并不是第一次,并且一开始就在readOnly中找到。
此时调用e.tryStore(&Value);
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 } } }
先判断取到的value是不是已经被删除的,如果是则return false;否则,调用cas替换原value的地址值指向新的value,则完成对read中的键值对的更新。如果在更新的过程中,value值被删除则return false;
情况三:如果在read中没找到,在dirty中找到,则加锁,加锁后还是要再load read(以防此时dirty晋升为read),从read中读到被标记删除的value则cas更新dirty中对应key的value设为nil,最后调用storePionter更改value的值。
func (e *entry) storeLocked(i *interface{}) { atomic.StorePointer(&e.p, unsafe.Pointer(i)) }
我们再来看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() }老样子,先从read中找,如果找到则返回value对应的数据。如果没找到,需要判断 amended是否为true,false的话说明dirty中没有多余数据,那找不到。如果为true,则去dirty中找,操作dirty需要先加锁,先判断read中有无(防止dirty晋升成read),找到返回,没找到再找dirty,如果找到那么给misses加一。
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 }如果misses长度大于等于dirty的长度,则晋升dirty为read,这步骤复杂度为O1,因为无非指针替换,再把dirty更新为nil,misses更新为0;