brief introduction
Gos built-in maps do not support concurrent write operations, because map write operations are not concurrently secure. When you try multiple Goroutine operations on the same map, an error occurs: fatal error: concurrent map writes.
So sync.Map has been officially introduced to accommodate concurrent programming applications.
The implementation of sync.Map can be summarized as follows:
- Read and write are separated by read and dirty fields, read-only fields exist for data read, and dirty fields exist for newly written data
- Read queries read, no dirty queries, write dirty only
- Read does not require a lock, but read or write dirty requires a lock
- There is also a misses field to count the number of times a read is penetrated (when penetration indicates the need to read dirty), and to synchronize dirty data to read more than a certain number of times
- Delay deletion by marking directly for deleted data
data structure
The data structure of Map is as follows:
type Map struct { // Locking protects dirty fields mu Mutex // Read-only data, the actual data type is readOnly read atomic.Value // Latest Written Data dirty map[interface{}]*entry // Counter, need to read dirty every time + 1 misses int }
The data structure of readOnly is:
type readOnly struct { // Built-in map m map[interface{}]*entry // Indicates that there is a key in dirty that is not in read, and this field determines whether to lock dirty amended bool }
The entry data structure is used to store pointers to values:
type entry struct { p unsafe.Pointer // Equivalent to *interface{} }
Attribute p has three states:
- p == nil: The key value has been deleted and m.dirty == nil
- p == expunged: The key value has been deleted, but m.dirty!=nil and m.dirty does not exist (expunged is actually a null interface pointer)
- In addition to the above, the key-value pair exists in m.read.m and m.dirty if m.dirty!=nil
Map s commonly use the following methods:
- Load: Read the specified key to return value
- Store: Store (add or change) key-value
- Delete: Delete the specified key
Source Parsing
Load
func (m *Map) Load(key interface{}) (value interface{}, ok bool) { // First try to read the readOnly object from read read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // Try to get from dirty if it doesn't exist if !ok && read.amended { m.mu.Lock() // Since the read fetch above is not locked, check again for safety read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // Get from dirty if it does not exist if !ok && read.amended { e, ok = m.dirty[key] // Call miss logic m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } // Read values from entry.p return e.load() } func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } // When miss accumulates too much, dirty is saved in read, amended = false, and m.dirty = nil m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
Store
func (m *Map) Store(key, value interface{}) { read, _ := m.read.Load().(readOnly) // If read exists, try saving to entry if e, ok := read.m[key]; ok && e.tryStore(&value) { return } // If the previous step is not successful, it will be handled on a case-by-case basis m.mu.Lock() read, _ = m.read.Load().(readOnly) // Like Load, get it again from read if e, ok := read.m[key]; ok { // Case 1: read exists if e.unexpungeLocked() { // If p == expunged, you need to assign entry to dirty first (because expunged data will not remain in dirty) m.dirty[key] = e } // Update entry with value e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { // Scenario 2: read does not exist, but dirty does exist, then update entry with value e.storeLocked(&value) } else { // Case 3: Neither read nor dirty exists if !read.amended { // If amended == false, call dirtyLocked to copy read to dirty (except for marked deleted data) m.dirtyLocked() // Then change amended to true m.read.Store(readOnly{m: read.m, amended: true}) } // Save new key values in dirty m.dirty[key] = newEntry(value) } m.mu.Unlock() } 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 } } } func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(&e.p, expunged, nil) } func (e *entry) storeLocked(i *interface{}) { atomic.StorePointer(&e.p, unsafe.Pointer(i)) } 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 { // Determine if entry is deleted or saved in dirty if !e.tryExpungeLocked() { m.dirty[k] = e } } } func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { // If there is p == nil (that is, the key-value pair is delete d), it will be set as expunged at this time if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
Delete
func (m *Map) Delete(key interface{}) { m.LoadAndDelete(key) } // LoadAndDelete acts as Delete and returns a value equal to whether it exists or not func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) { // Getting logic is similar to Load, querying dirty if read does not exist 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 { e, ok = m.dirty[key] m.missLocked() } m.mu.Unlock() } // Delete after querying entry if ok { // Marking entry.p as nil does not actually delete the data // Truly deleting data and being set as expunged is in Store's tryExpungeLocked return e.delete() } return nil, false }
summary
It can be seen that this read-write separation design solves the write security in concurrent situations, and makes the read speed close to the built-in map in most cases, which is very suitable for cases with more reads and less writes.
sync.Map has other methods:
- Range: Traverses through all key-value pairs and the parameter is a callback function
- LoadOrStore: Read data, save it and read it if it doesn't exist
This is no longer detailed here, see Source code.