Go语言在sync包中给出了线程安全的map。
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
}
Map的结构体很简单,通过Mutex来作为加锁的工具,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.
}
readOnly的结构很简单,一个内置map和一个bool类型的amended,其中readOnly的map在读取数据的时候不需要加锁,在读取数据的时候也会相对的优先从这里的map进行寻找数据。而相应的,Map结构中还有内置map类型的dirty,其中在dirty中读取数据需要加锁,在读取数据的时候会优先从readOnly中的map找,如果没找到,在在dirty中找,如果在dirty中找到了,则会将Map中int类型的missing加一,当missing超过dirty的大小的时候,则会将dirty的数据复制到readOnly中的map中完成升级。而readOnly中bool类型的amended则表示dirty中有readOnly中没有的数据。
而在Map中,存在一个expunged指针用来作为将要被移除的键值对。而entry结构体就很简单,只有一个指向value地址的指针。
var expunged = unsafe.Pointer(new(interface{}))
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{}
}
在sync中的map中,通过Store()方法存储键值对。
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()
当Map刚刚通过new()方法建立的时候,还并没有保存任何数据,这个时候,调用Store()传入键值对准备放入Map中。首先会通过atomic.Value的Load()方法取得readOnly,当然此时的readOnly并没有任何数据,所以接下来在readOnly中找不到相应的键值对,那么将会去dirty去寻找相应的键,对于readOnly并不需要加锁,但是一旦涉及到dirty就需要调用Mutex进行加锁,如果找不到,则会判断readOnly中的amended是否为false,用来确认是不是第一次在dirty中添加新的key,由于readOnly刚刚被创建,所有其必然是默认的false,那么则会调用dirtyLocked()方法初始化dirty这个map。
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
}
}
}
在这个方法中,将会创建新的map作为dirty,并且遍历readOnly中的数据将其复制在dirty中。
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
}
但是在复制之前调用tryExpungeLocked()方法保证该键值对不是已经被删除的键值对。
在创建完毕dirty之后,就会创建新的readOnly赋给read,同时amended为true,因为此时dirty将会有第一个键值对,而此时readOnly中并没有数据。
而后在dirty中创建key与value地址的指针的键值对,而后给Mutex解锁。
但是如果存放键值对一开始,就在readOnly中找到了相应的键,那么就需要调用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
}
}
}
首先会取得key所对应的entry也就是里面的指针,如果已经指向expunged,那么说明该值已经被删除,那么就需要返回false重新返回false,重新对dirty进行操作。而如果不是,那么直接通过cas将原本value地址的指针指向新的value地址就完成了对于read中键值对的更新。
那么,如果readOnly中没有找到,在dirty中找到了呢,那么还是需要加锁,加锁之后还有去readOnly中再找一次,防止多线程下dirty晋升为read,导致read数据与之前不一致。如果在第二次对readOnly检查中找到了相应的键值对,那么首先确保并不是被删除逇键值对,如果已经被删除,那么先在dirty中更新key对应的空指针,然后调用storeLocked()方法更新entry中的指针指向新的value地址。
如果在加锁的情况下仍旧没有在readOnly中找到,那么如果在dirty中找到,那么就更新key所对应的entry对应的指针就可以了。
以上是Map中键值对的存储,而键值对的读取则通过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()
}
当需要根据给出的key取value的时候,首先去readOnly中去寻找相应的键值对,如果有,直接返回。如果没有找到,则需要判断amended是否为true,如果为false说明dirty没有read,没有的数据,那么也没有必要再去dirty找。如果为true,则证明有需要去dirty的必要。那么加锁,继续检查一次read,避免在这期间dirty晋升为read。而后去dirty找,如果找到了,那么就调用missLocked()给miss加一。
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
}
如果miss大于dirty的长度,则有必要将此时read的数据晋升到read中,那么重新创建一个readOnly,其中的map就是dirty,amended则默认为true,因为新的read的内容就是此时的dirty,两者没有区别。之后清空dirty,并将miss变为0。
在完成之后,返回找到的指针对应的地址下的数据。