Deep understanding of Go 1.9 sync. Map

Source: Internet
Author: User
This is a creation in Article, where the information may have evolved or changed.

The official Go FAQ already mentions that the built-in map is not thread-safe (goroutine). Before go 1.6, the built-in map type is partially goroutine secure, concurrent reads are not problematic, and concurrent writes may be problematic. Since go 1.6, the concurrent read and write map will be an error, this problem in some well-known open source libraries, so the solution before go 1.9 is to bind a lock, packaged into a new struct or use a separate lock. In addition, the author usually uses Concurrent-map to solve these problems before go 1.9, but not all third-party libraries solve the problem.

Let's take a look at this code sample: The program in a goroutine has been read, a goroutine has been writing the same key value, even if the key is not the same, and map does not "expand" and other operations, the code will be error, 错误信息是: 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 {}}

The source of the problem in the Go source code: hashmap_fast.go#l118, will see the reading will check the hashwriting flag, if there is this flag, will be reported concurrency error.

This flag is set when it is written: hashmap.go#l542

h.flags |= hashWriting

This flag is canceled when the hashmap.go#l628 is set. This kind of concurrent read and write check there are many, such as when writing will also check whether there is concurrent write, delete the key when similar to write, traverse the time of concurrent reading and writing problems. Map concurrency problems are not so easy to detect, you can use the-race parameter to check.

The concurrent use of Map objects is a common requirement in our daily development, especially in some large projects. Map always saves Goroutine shared data. Before go 1.9 in the Go official blog Go maps in action article, gives a simple solution.

First, add a read-write lock to the map by embedding the struct

var counter = struct{    sync.RWMutex    m map[string]int}{m: make(map[string]int)}

Can be easily locked when reading and writing data

counter.RLock()n := counter.m["some_key"]counter.RUnlock()fmt.Println("some_key:", n)counter.Lock()counter.m["some_key"]++counter.Unlock()

Of course, you can also use Concurrent-map to solve problems.

// Create a new map.map := cmap.New()// Sets item within map, sets "bar" under key "foo"map.Set("foo", "bar")// Retrieve item from map.if tmp, ok := map.Get("foo"); ok {bar := tmp.(string)}// Removes item under key "foo"map.Remove("foo")

Both are essentially used sync.RWMutex to secure threads (goroutine). This solution is fairly concise, and using a read-write lock instead of a mutex can further reduce read and write performance due to locking. But in the case of a very large map data, a lock can cause large concurrent clients to compete for a lock, then sync in Go 1.9. Map is very practical. (in addition to these, there is a library that I would like to mention, CMap is also a very good, safe and excellent performance of the third party libraries)

Go 1.9 in sync. The implementation of map has the following optimization points:

    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.
    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.

Sync. The map data structure is simple and contains four fields:,,, read mu dirty misses .

type Map struct {// 当涉及到dirty数据的操作的时候,需要使用此锁mu Mutex// 一个只读的数据结构,因为只读,所以不会有读写冲突。// 所以从这个数据中读取总是安全的。// 实际上,实际也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。read atomic.Value // readOnly// dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。// 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。// 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。dirty map[interface{}]*entry// 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,// 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。misses int}

readThe data structure

type readOnly struct {m       map[interface{}]*entryamended bool // 如果Map.dirty有些数据不在其中的时候,这个值为true}

The essence of this is the use of redundant data read structures dirty . dirtywill contain read entries that are not deleted, and the newly added entries will be added to the dirty . amendedindicates that Map.dirty there is readOnly.m no data contained in it, so if you cannot find the Map.read data, you will need to find it further Map.dirty . And the Map.read modification is done by atomic manipulation. Although read dirty there are redundant data, the data is pointed to the same data through pointers, so although the value of map is large, the space consumption of redundancy is still limited. readOnly.mand Map.dirty The stored value type is *entry , it contains a pointer to P, which points to the value stored by the user.

type entry struct {p unsafe.Pointer // *interface{}}

P has three kinds of values:

    • Nil:entry has been removed, and M.dirty is nil
    • Expunged:entry has been removed, and m.dirty is not nil, and this entry does not exist in M.dirty
    • Other: entry is a normal value

Understood the sync. Map's data structure, let's look at sync first. Map's Load Method implementation

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {// 1.首先从m.read中得到只读readOnly,从它的map中查找,不需要加锁read, _ := m.read.Load().(readOnly)e, ok := read.m[key]// 2. 如果没找到,并且m.dirty中有新数据,需要从m.dirty查找,这个时候需要加锁if !ok && read.amended {m.mu.Lock()// 双检查,避免加锁的时候m.dirty提升为m.read,这个时候m.read可能被替换了。read, _ = m.read.Load().(readOnly)e, ok = read.m[key]// 如果m.read中还是不存在,并且m.dirty中有新数据if !ok && read.amended {// 从m.dirty查找e, ok = m.dirty[key]// 不管m.dirty中存不存在,都将misses计数加一// missLocked()中满足条件后就会提升m.dirtym.missLocked()}m.mu.Unlock()}if !ok {return nil, false}return e.load()}

The load Load method, which provides a key to find the corresponding value, if it does not exist, is reflected by OK. The essence here is to load from m.read, not present, and m.dirty with new data, lock it, and load it from M.dirty. The other point is that double-check processing is used here, because in the following two statements, these two lines of statements are not an atomic operation.

if !ok && read.amended {m.mu.Lock()

Although the first sentence of the implementation of the condition is satisfied, but before the lock, m.dirty may be promoted to m.read , so after the lock has to be checked again m.read , the following methods are used in this method. If we query the key value just exists m.read in, then no lock, direct return, theoretically excellent performance. Even if it does not exist m.read in, after Miss several times, m.dirty it will be promoted to m.read , and will be looked up from m.read . So for the update/add less, load the existence of the key in a lot of scenes, the performance of basic and lock-free map is comparable.

After miss a few times, m.dirty will be promoted to m.read , then m.dirty how to be promoted? The emphasis is on the misslocked method.

func (m *Map) missLocked() {m.misses++if m.misses < len(m.dirty) {return}m.read.Store(readOnly{m: m.dirty})m.dirty = nilm.misses = 0}

The last three lines of code are the ascending m.dirty , very simple m.dirty readOnly fields that will be updated as m atoms m.read . Post-elevation m.dirty , reset m.misses , and m.read.amended false.

Sync. Map's Store method implementation

Func (M *map) store (key, Value interface{}) {//If m.read exists with this key and this entry is not marked for deletion, try to store it directly. Because M.dirty also points to this entry, so M.dirty also keep the latest entry. Read, _: = M.read.load (). (readOnly) If e, OK: = Read.m[key]; OK && E.trystore (&value) {return}//if ' m.read ' does not exist or has been flagged for deletion m.mu.lock () read, _ = M.read.load (). (readOnly) If e, OK: = Read.m[key]; OK {if e.unexpungelocked () {//marked as not deleted m.dirty[key] = This key does not exist in E//m.dirty, so join m.dirty}e.storelocked (&value)//update} else if e, OK: = M.dirty[key]; OK {//M.dirty exists this key, update e.storelocked (&value)} else {//new key value if!read.amended {// No new data in M.dirty, add first new key to M.dirty m.dirtylocked ()//copy deleted data from M.read M.read.store (READONLY{M:READ.M, amended:true})} M.dirty[key] = newEntry (value)//Add this entry to M.dirty}m.mu.unlock ()}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}}}func (e *entry) tryexpungelocked () (Isexpunged boOL) {p: = atomic. Loadpointer (&AMP;E.P) for p = = Nil {//will have deleted data marked as nil marked as Expungedif atomic. Compareandswappointer (&AMP;E.P, nil, expunged) {return true}p = atomic. Loadpointer (&AMP;E.P)}return p = = expunged}

The store method is to update or add a entry. The above operation starts from the operation first m.read , does not satisfy the condition to lock again, then the operation m.dirty . The store may be able to replicate data from M.read in some cases (either initialized or m.dirty just promoted), which can affect performance if the amount of data in M.read is very large.

Sync. The implementation of the Delete method of map

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 {delete(m.dirty, key)}m.mu.Unlock()}if ok {e.delete()}}func (e *entry) delete() (hadValue bool) {for {p := atomic.LoadPointer(&e.p)// 已标记为删除if p == nil || p == expunged {return false}// 原子操作,e.p标记为nilif atomic.CompareAndSwapPointer(&e.p, p, nil) {return true}}}

The Delete method deletes a key value. As with the store method, the delete operation starts from the m.read middle, and if the entry does not exist in and m.read there is m.dirty new data in it, the lock attempt is m.dirty removed from. Be careful, or double check. From m.dirty the direct deletion can be, when it does not exist, but if it is m.read deleted from the, and will not be deleted directly, but to hit the mark.

Sync. The Range method of map is implemented

func (m *Map) Range(f func(key, value interface{}) bool) {read, _ := m.read.Load().(readOnly)// 如果m.dirty中有新数据,则提升m.dirty,然后在遍历if read.amended {//提升m.dirtym.mu.Lock()read, _ = m.read.Load().(readOnly) //双检查if read.amended {read = readOnly{m: m.dirty}m.read.Store(read)m.dirty = nilm.misses = 0}m.mu.Unlock()}// 遍历, for range是安全的for k, e := range read.m {v, ok := e.load()if !ok {continue}if !f(k, v) {break}}}

In the go language, for ... range map it is built-in language features, so there is no way to use for range traverse sync. Map, so the workaround has a range method, which is traversed by a callback. A m.dirty promotion may be made before the range method is called, but lifting M.dirty is not a time-consuming operation.

Sync. Loadorstore method Implementation of Map

Func (M *map) Loadorstore (key, Value interface{}) (actual interface{}, loaded bool) {Read, _: = M.read.load (). ( readOnly) If e, OK: = Read.m[key]; OK {Actual, loaded, OK: = E.tryloadorstore (value) If OK {return actual, Loaded}}m.mu.lock () Read, _ = M.read.load (). (readOnly) If e, OK: = Read.m[key]; OK {if e.unexpungelocked () {M.dirty[key] = E}actual, loaded, _ = E.tryloadorstore (value)} else if e, OK: = M.dirty[key]; OK {Actual, loaded, _ = E.tryloadorstore (value) m.misslocked ()} else {if!read.amended {//Add a new dirty tag to key,//read-only as incomplete m.dir Tylocked () M.read.store (READONLY{M:READ.M, amended:true})}m.dirty[key] = newEntry (value) actual, Loaded = value, false} M.mu.unlock () return actual, Loaded}func (e *entry) tryloadorstore (i interface{}) (actual interface{}, loaded, OK bool) {P : = Atomic. Loadpointer (&AMP;E.P) If p = = expunged {return nil, false, false}if P! = Nil {return * (*interface{}) (P), true, true}ic: = i for {if atomic.compareandswappointer (&e.p, nil, unsafe. Pointer (&ic)) {return I, FALSE, true}p = atomic. Loadpointer (&AMP;E.P) If p = = expunged {return nil, false, false}if P! = Nil {return * (*interface{}) (P), True, True}}}

The Loadorstore method returns the existing value (Load) If the supplied key exists, otherwise saves the provided key value (Store). The same m.read starts, then is m.dirty , and finally there is a double check mechanism.

Performance testing is provided in the Go 1.9 source code: Map_bench_test.go, Map_reference_test.go, and compared to previous solutions, there are a number of improvements to performance.

Last sync. Map does not have the Len method, and there is currently no indication to add (issue#20680), so if you want to get the number of valid entries in the current map, you need to traverse it using the Range method.

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.