Have to know Golang's Sync.map source analysis

Source: Internet
Author: User

Sync. Map Source Analysis

Background

As we all know, go normal map is not support concurrency, in other words, not the thread (goroutine) security. The blogger was used from Golang 1.4, when the concurrent read of the map was not supported, but the concurrent write would show dirty data. After Golang 1.6, the concurrent read and write will be panic directly:

fatal error: concurrent map read and map write
package mainfunc main() {    m := make(map[int]int)    go func() {        for {            _ = m[1]        }    }()    go func() {        for {            m[2] = 2        }    }()    select {}}

So when you need to support concurrent read and write to map, bloggers use two methods:

    1. Third-party class library Concurrent-map.
    2. Map plus sync. Rwmutex to secure the thread (goroutine).

After Golang 1.9, go has introduced a concurrency-safe map under the sync package, as well as a third method for bloggers. This paper focuses on this, in order to timeliness, this article based on Golang 1.10 source code for analysis.

Sync. Map

Structural body

Map

type Map struct {    mu Mutex    //互斥锁,用于锁定dirty map    read atomic.Value //优先读map,支持原子操作,注释中有readOnly不是说read是只读,而是它的结构体。read实际上有写的操作    dirty map[interface{}]*entry // dirty是一个当前最新的map,允许读写    misses int // 主要记录read读取不到数据加锁读取read map以及dirty map的次数,当misses等于dirty的长度时,会将dirty复制到read}

ReadOnly

ReadOnly is primarily used for storing elements stored in map.read by atomic manipulation.

type readOnly struct {    m       map[interface{}]*entry    amended bool // 如果数据在dirty中但没有在read中,该值为true,作为修改标识}

Entry

type entry struct {    // nil: 表示为被删除,调用Delete()可以将read map中的元素置为nil    // expunged: 也是表示被删除,但是该键只在read而没有在dirty中,这种情况出现在将read复制到dirty中,即复制的过程会先将nil标记为expunged,然后不将其复制到dirty    //  其他: 表示存着真正的数据    p unsafe.Pointer // *interface{}}

Principle

If you've been exposed to big Java, then you must have added the number of locks to cocurrenthashmap using the lock segmentation technique , so that the principle of controlling the number of threads competing for the same lock is remembered deeply.
So is Golang's sync.map using the same principle? Sync. The principle of map is very simple, the use of space-time strategy, through the redundant two data structures (read, dirty), to achieve the effect of lock on performance.
By introducing two maps to separate the read and write into different maps, where read map provides concurrent read and existing element Atomic writes, and dirty map is responsible for reading and writing. In this way, the read map can be read concurrently without locking, and when no value is read in the read map, then the lock is added for subsequent reads and the number of misses is incremented, and when the number of misses is greater than or equal to the dirty map length, the dirty map is raised to read map. From the previous definition of the structure can be found, although the introduction of two map, but the underlying data storage is a pointer, pointing to the same value.

Sync at the beginning. Map writes data

X=1Y=2Z=3

Dirty map mainly accepts write requests, and read map has no data, at this time read map with dirty map data such as.

Reads from the read map when reading the data, at which time the read map does not have data, Miss records the number of reads from the read map failed, when Misses>=len (Dirty map), dirty Map is directly upgraded to read map, A copy of the address is directly made to the dirty map and the dirty map is emptied, and the misses is set to 0. At this point read map with dirty map data such as.

There is now a need to modify the Z element Z=4,sync. Map will modify the elements of the read map directly.

New Canadian k=5, the new element will need to operate dirty map, if the misses to reach the threshold dirty map directly upgrade to read map and dirty map is an empty map (read amended==false), then dirty Map needs to copy data from read map.

The effect of the upgrade is as follows.

If you need to delete Z, you need to do several things:
A read map that has this element and read Amended==false: Directly set the element in read to nil.

The other is that the element was just written to dirty map and was not upgraded to read map: Call Golang built-in function directly to delete the dirty map element;

Another is that the read map and dirty map have the same element: the elements in the read map are set to nil because both the read map and the dirty map use the element addresses, so they are all set to nil.

Optimization point

    1. Space change time. With redundant two data structures (read, dirty), the effect of locking on performance is realized.
    2. Use read-only data to avoid read-write conflicts.
    3. Dynamic adjustment, after miss the number of times, the dirty data is promoted to read.
    4. double-checking (double detection).
    5. Deferred deletion. Deleting a key is only a marker, and the deleted data is only cleaned up when the dirty is lifted.
    6. Read, update, delete in priority from read, because no lock is required for reading read.

Method source Code Analysis

Load

Load returns the key value stored in the map and returns nil if there is no value. The OK result indicates whether the value is found in the map.

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {    // 第一次检测元素是否存在    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]    if !ok && read.amended {        // 为dirty map 加锁        m.mu.Lock()        // 第二次检测元素是否存在,主要防止在加锁的过程中,dirty map转换成read map,从而导致读取不到数据        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            // 从dirty map中获取是为了应对read map中不存在的新元素            e, ok = m.dirty[key]            // 不论元素是否存在,均需要记录miss数,以便dirty map升级为read map            m.missLocked()        }        // 解锁        m.mu.Unlock()    }    // 元素不存在直接返回    if !ok {        return nil, false    }    return e.load()}

Dirty map upgrade to read map

func (m *Map) missLocked() {    // misses自增1    m.misses++    // 判断dirty map是否可以升级为read map    if m.misses < len(m.dirty) {        return    }    // dirty map升级为read map    m.read.Store(readOnly{m: m.dirty})    // dirty map 清空    m.dirty = nil    // misses重置为0    m.misses = 0}

Element value

func (e *entry) load() (value interface{}, ok bool) {    p := atomic.LoadPointer(&e.p)    // 元素不存在或者被删除,则直接返回    if p == nil || p == expunged {        return nil, false    }    return *(*interface{})(p), true}

Read map is used primarily for reading, and each time load is read from read, and when read does not exist and amended is true, the data is read from dirty. Regardless of whether the element exists in the dirty map, the misslocked function is executed, and the function misses+1, when the m.misses &lt; len(m.dirty) dirty is copied to read, then dirty is set to nil,misses=0.

Storage

Set the Key=>value.

Func (M *map) Store (key, Value interface{}) {//If read exists this key, and this entry is not marked for deletion, attempts to write directly, writes successfully, then ends//First detection Read, _: = M.read.load (). (readOnly) If e, OK: = Read.m[key]; OK && E.trystore (&value) {return}//Dirty map Lock M.mu.lock ()//second detection read, _ = M.read. Load (). (readOnly) If e, OK: = Read.m[key]; OK {//UNEXPUNGELOCC ensure that the element is not marked for deletion//judgment element is identified as Delete if e.unexpungelocked () {//This element was deleted before, which means            There is a non-nil dirty, this element is not inside. M.dirty[key] = e}//update read MAP element value e.storelocked (&value)} else if e, OK: = M.dirty[key];  OK {//At this time the read map does not have the element, but dirty map has the element and needs to modify the dirty map element value to the latest value e.storelocked (&value)} else {//            Read.amended==false, stating that dirty map is empty, you need to copy the read map to dirty map if!read.amended {m.dirtylocked () Set Read.amended==true, description dirty map has data m.read.store (READONLY{M:READ.M, Amended:true})}// Setting elements into dirtyMap, at which point dirty map has the read map and the latest set of elements M.dirty[key] = NewEntry (value)}//Unlocked, some people think that the lock range is a bit large, assuming that the read map data is large, then execute M.dir Tylocked () can take a lot of time, and it's perfectly possible to lock the dirty map, so the idea is wrong, because there's a write operation in m.dirtylocked () M.mu.unlock ()}

Try to store the element.

func (e *entry) tryStore(i *interface{}) bool {    // 获取对应Key的元素,判断是否标识为删除    p := atomic.LoadPointer(&e.p)    if p == expunged {        return false    }    for {        // cas尝试写入新元素值        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {            return true        }        // 判断是否标识为删除        p = atomic.LoadPointer(&e.p)        if p == expunged {            return false        }    }}

UNEXPUNGELOCC ensures that the element is not marked for deletion. If the element was previously deleted, it must be added to the dirty map before it is unlocked.

func (e *entry) unexpungeLocked() (wasExpunged bool) {    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)}

Copy from read map to 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 {        // 如果标记为nil或者expunged,则不复制到dirty map        if !e.tryExpungeLocked() {            m.dirty[k] = e        }    }}

Loadorstore

If the corresponding element is present, the value of the element is returned, and if it does not exist, the element is written to sync. Map. If the value is loaded, the load result is true, or false if it is already stored.

Func (M *map) Loadorstore (key, Value interface{}) (actual interface{}, loaded bool) {/////without lock read-MAP//first-time detection Read, _: = M.read.load (). (readOnly) If e, OK: = Read.m[key];         OK {//If the element exists (is identified as deleted by Tryloadorstore for processing), try to get the value that the element already exists or write the element to actual, loaded, OK: = E.tryloadorstore (value) If OK {return actual, Loaded}} m.mu.lock ()//second detection//the following logic see store read, _ = M . read. Load (). (readOnly) If e, OK: = Read.m[key]; OK {if e.unexpungelocked () {M.dirty[key] = e} actual, Loaded, _ = E.tryloadorstore (Valu E)} else if e, OK: = M.dirty[key];            OK {Actual, loaded, _ = E.tryloadorstore (value) m.misslocked ()} else {if!read.amended { M.dirtylocked () M.read.store (READONLY{M:READ.M, Amended:true})} M.dirty[key] = NewEntry (v Alue) Actual, Loaded = value, false} m.mu.unlock () return actual, Loaded}

If no element is deleted, Tryloadorstore will automatically load or store a value. If you delete an element, Tryloadorstore keeps the entry intact and returns ok= false.

func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {    p := atomic.LoadPointer(&e.p)    // 元素标识删除,直接返回    if p == expunged {        return nil, false, false    }    // 存在该元素真实值,则直接返回原来的元素值    if p != nil {        return *(*interface{})(p), true, true    }    // 如果p为nil(此处的nil,并是不是指元素的值为nil,而是atomic.LoadPointer(&e.p)为nil,元素的nil在unsafe.Pointer是有值的),则更新该元素值    ic := i    for {        if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {            return i, false, true        }        p = atomic.LoadPointer(&e.p)        if p == expunged {            return nil, false, false        }        if p != nil {            return *(*interface{})(p), true, true        }    }}

Delete

Delete elements, deferred deletion, when the read map element is present, the element is set to nil, only in the promotion of dirty to clean up the number of deletes, deferred deletion can avoid subsequent acquisition of deleted elements need to lock. Delete an element in the dirty map directly when the read map does not exist

func (m *Map) Delete(key interface{}) {    // 第一次检测    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 {            // 不论dirty map是否存在该元素,都会执行删除            delete(m.dirty, key)        }        m.mu.Unlock()    }    if ok {        // 如果在read中,则将其标记为删除(nil)        e.delete()    }}

Element value is nil

func (e *entry) delete() (hadValue bool) {    for {        p := atomic.LoadPointer(&e.p)        if p == nil || p == expunged {            return false        }        if atomic.CompareAndSwapPointer(&e.p, p, nil) {            return true        }    }}

Range

Traverse to get sync. All the elements in the map are used as snapshots, so they are not necessarily accurate.

func (m *Map) Range(f func(key, value interface{}) bool) {    // 第一检测    read, _ := m.read.Load().(readOnly)    // read.amended=true,说明dirty map包含所有有效的元素(含新加,不含被删除的),使用dirty map    if read.amended {        // 第二检测        m.mu.Lock()        read, _ = m.read.Load().(readOnly)        if read.amended {            // 使用dirty map并且升级为read map            read = readOnly{m: m.dirty}            m.read.Store(read)            m.dirty = nil            m.misses = 0        }        m.mu.Unlock()    }    // 一贯原则,使用read map作为读    for k, e := range read.m {        v, ok := e.load()        // 被删除的不计入        if !ok {            continue        }        // 函数返回false,终止        if !f(k, v) {            break        }    }}

Summarize

Through the above analysis can be obtained, sync. Map is not suitable for a large number of simultaneous read and write scenarios, a large number of writes will cause the read map reading data to lock for further reading, while the dirty map has been upgraded to read map. This results in a lower overall performance, especially for the cache scenario. Use sync for append-only and a large number of read, small write scenes. Map is relatively appropriate.

Sync. Map does not provide a Len () method to get the number of elements, but can be achieved by using range ().

func Len(sm sync.Map) int {    lengh := 0    f := func(key, value interface{}) bool {        lengh++        return true    }    one:=lengh    lengh=0    sm.Range(f)    if one != lengh {        one = lengh        lengh=0        sm.Range(f)        if one <lengh {            return lengh        }    }    return one}

Reference

    • Go sync. Map
    • Go 1.9 Sync. Map Secrets
Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.