Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to use Go to read and write sync.map statements concurrently

2025-01-19 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly introduces "how to use Go to read and write sync.map statements concurrently". In daily operation, I believe many people have doubts about how to use Go to read and write sync.map statements concurrently. The editor consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the doubts of "how to use Go to read and write sync.map sentences concurrently". Next, please follow the editor to study!

Catalogue

1. Advantages of sync.Map

2. Performance testing

2.1 pressure test results

1) write

2) find

3) Delete

2.3 scene analysis

3. Sync.Map analysis

3.1 data structure

3.2 search process

3.3 write proc

3.4 deletion process

The two most commonly used concurrency support modes of map in the industry are:

Native map + mutex or read-write lock mutex.

Standard library sync.Map (Go1.9 and beyond).

With choice, there is always choice difficulty, how to choose these two kinds in the end, whose performance is better? A friend of mine said that the performance of the standard library sync.Map is very good. Don't use it. Who do I listen to?

Today, fried fish will take you to reveal the secrets of Go sync.map, we will first understand what scenarios, many types of Go map how to use, whose performance is the best!

Then, according to the results of the performance analysis of each map, the source code of sync.map is dissected to understand WHY.

Let's start to smoke fish happily together.

1. Advantages of sync.Map

Some suggestions for clearly pointing out the Map type in the Go official documentation:

The concurrent use of multiple goroutine is safe and does not require additional locking or coordination control.

Most code should use native map rather than individual locking or coordination controls for better type safety and maintainability.

At the same time, the Map type is optimized for the following scenarios:

When an entry for a given key is written only once but read multiple times. For example, in a cache that will only grow, there will be such a business scenario.

When multiple goroutines reads, writes, and overwrites entries of unrelated key sets.

In both cases, compared with Go map with a separate Mutex or RWMutex, the use of Map types can greatly reduce lock contention.

2. Performance testing

After listening to the official documents to introduce a number of benefits, he did not talk about the disadvantages, and whether the advantages of the optimized performance are true and credible. Let's verify it together.

First, we define the basic data structure:

/ on behalf of the mutex type FooMap struct {sync.Mutex data map [int] int} / on behalf of the read-write lock type BarRwMap struct {sync.RWMutex data mapping int} var fooMap * FooMapvar barRwMap * BarRwMapvar syncMap * sync.Map// initializing the basic data structure func init () {fooMap = & FooMap {data: make} barRwMap = & BarRwMap {data: make} syncMap = & sync.Map {}

In the supporting methods, we have compiled the corresponding methods for the common actions of adding, deleting, modifying and checking. For subsequent stress tests (only part of the code is shown):

Func builtinRwMapStore (k, v int) {barRwMap.Lock () defer barRwMap.Unlock () barRwMap.data [k] = v} func builtinRwMapLookup (k int) int {barRwMap.RLock () defer barRwMap.RUnlock () if v, ok: = barRwMap.data [k];! ok {return-1} else {return v}} func builtinRwMapDelete (k int) {barRwMap.Lock () defer barRwMap.Unlock () if _, ok: = barRwMap.data [k]; ok {return} else {delete (barRwMap.data, k)}}

The rest of the type methods are basically similar, and the question of repetition is considered, so it is not shown here.

The basic code of the pressure test method is as follows:

Func BenchmarkBuiltinRwMapDeleteParalell (b * testing.B) {b.RunParallel (func (pb * testing.PB) {r: = rand.New (rand.NewSource (time.Now (). Unix () for pb.Next () {k: = r.Intn (100000000) builtinRwMapDelete (k)})}

This piece is mainly to add, delete, change and check the code and the preparation of the pressure test method, the pressure test code is directly reused is the big white boss's go19-examples/benchmark-for-map project.

You can also use the official map\ _ bench\ _ test.go provided by Go. Interested partners can pull it down and try it out.

2.1 pressure test result 1) write name meaning pressure test result BenchmarkBuiltinMapStoreParalell-4map+mutex write element 237.1 ns/opBenchmarkSyncMapStoreParalell-4sync.map write element 509.3 ns/opBenchmarkBuiltinRwMapStoreParalell-4map+rwmutex write element 207.8 ns/op

The overall sort (from slow to fast) is: SyncMapStore

< MapStore < RwMapStore。 2)查找方法名含义压测结果BenchmarkBuiltinMapLookupParalell-4map+mutex 查找元素166.7 ns/opBenchmarkBuiltinRwMapLookupParalell-4map+rwmutex 查找元素60.49 ns/opBenchmarkSyncMapLookupParalell-4sync.map 查找元素53.39 ns/op 在查找元素上,最慢的是原生 map+互斥锁,其次是原生 map+读写锁。最快的是 sync.map 类型。 总体的排序为:MapLookup < RwMapLookup < SyncMapLookup。 3)删除方法名含义压测结果BenchmarkBuiltinMapDeleteParalell-4map+mutex 删除元素168.3 ns/opBenchmarkBuiltinRwMapDeleteParalell-4map+rwmutex 删除元素188.5 ns/opBenchmarkSyncMapDeleteParalell-4sync.map 删除元素41.54 ns/op 在删除元素上,最慢的是原生 map+读写锁,其次是原生 map+互斥锁,最快的是 sync.map 类型。 总体的排序为:RwMapDelete < MapDelete < SyncMapDelete。 2.3 场景分析 根据上述的压测结果,我们可以得出 sync.Map 类型: 在读和删场景上的性能是最佳的,领先一倍有多。 在写入场景上的性能非常差,落后原生 map+锁整整有一倍之多。 因此在实际的业务场景中。假设是读多写少的场景,会更建议使用 sync.Map 类型。 但若是那种写多的场景,例如多 goroutine 批量的循环写入,那就建议另辟途径了,性能不忍直视(无性能要求另当别论)。 3、sync.Map 剖析 清楚如何测试,测试的结果后。我们需要进一步深挖,知其所以然。 为什么 sync.Map 类型的测试结果这么的 "偏科",为什么读操作性能这么高,写操作性能低的可怕,他是怎么设计的? 3.1 数据结构 sync.Map 类型的底层数据结构如下: type Map struct { mu Mutex read atomic.Value // readOnly dirty map[interface{}]*entry misses int}// Map.read 属性实际存储的是 readOnly。type readOnly struct { m map[interface{}]*entry amended bool} mu:互斥锁,用于保护 read 和 dirty。 read:只读数据,支持并发读取(atomic.Value 类型)。如果涉及到更新操作,则只需要加锁来保证数据安全。read 实际存储的是 readOnly 结构体,内部也是一个原生 map,amended 属性用于标记 read 和 dirty 的数据是否一致。 dirty:读写数据,是一个原生 map,也就是非线程安全。操作 dirty 需要加锁来保证数据安全。 misses:统计有多少次读取 read 没有命中。每次 read 中读取失败后,misses 的计数值都会加 1。 在 read 和 dirty 中,都有涉及到的结构体: type entry struct { p unsafe.Pointer // *interface{}} 其包含一个指针 p, 用于指向用户存储的元素(key)所指向的 value 值。 在此建议你必须搞懂 read、dirty、entry,再往下看,食用效果会更佳,后续会围绕着这几个概念流转。 3.2 查找过程 划重点,Map 类型本质上是有两个 "map"。一个叫 read、一个叫 dirty,长的也差不多:

2 map of sync.Map

When we read data from the sync.Map type, it first checks to see if the read contains the required elements:

If so, the data is read and returned through the atomic atomic operation.

If not, the amended attribute in read.readOnly is determined, and he will tell the program whether dirty contains data that is not in read.readOnly.m; so if it does, that is, amended is true, it will further look for data in dirty.

The reason why the read performance of sync.Map is so high lies in the ingenious design of read, which, as a cache layer, provides fast path (fast path) lookup.

At the same time, combined with the amended attribute, it solves the problem that locks are involved in each read, and realizes the high performance of reading this usage scenario.

3.3 write proc

We focus directly on the Store method of type sync.Map, which adds or updates an element.

The source code is as follows

Func (m * Map) Store (key, value interface {}) {read, _: = m.read.Load (). (readOnly) if e, ok: = read.m [key]; ok & & e.tryStore (& value) {return}.}

Call the Load method to check if this element exists in the m.read. If it exists and is not marked as deleted, try to store it.

If the element does not exist or has been marked as deleted, proceed to the following process:

Func (m * Map) Store (key, value interface {}) {... M.mu.Lock () read, _ = m.read.Load (). (readOnly) if e, ok: = read.m [key]; ok {if e.unexpungeLocked () {m.dirty [key] = e} e.storeLocked (& value)} else if e, ok: = m.dirty [key] Ok {e.storeLocked (& value)} else {if! read.amended {m.dirtyLocked () m.read.Store (readOnly {m: read.m, amended: true})} m.dirty [key] = newEntry (value)} m.mu.Unlock ()}

Since we have come to the dirty process, the mutex on the Lock method is called directly at the beginning to ensure data security, which is also the first act to highlight the deterioration of performance.

It is divided into three processing branches:

If the element is found in read but has been marked as deleted (expunged), dirty is not equal to nil (it certainly does not exist in dirty). It will do the following.

Change the element state from expunged to nil.

Insert the element into the dirty.

If it is found that the element does not exist in read, but it exists in dirty, the direction of the updated entry is written directly.

If it is found that the element does not exist in both read and dirty, the data that has not been deleted by the tag is copied from the read, and the element is inserted into the dirty, giving the element value entry the direction.

Let's get this straight. The whole process of the writing process is:

Check the read,read, or the deletion status has been marked.

Apply a mutex (Mutex).

Operation dirty, according to a variety of data conditions and status processing.

Back to the original topic, why his writing performance is so poor. The reason is:

Write must go through read, in any case, one more layer than others, and then check the data situation and status, the performance overhead is greater.

(the third processing branch) after initialization or dirty promotion, all the data will be copied from the read. If the amount of data in the read is large, it will affect the performance.

You can know that the sync.Map type is not suitable for scenarios with more writes, and it is better to read more and write less.

If there is a scenario with a large amount of data, you need to consider whether the occasional performance jitter when read replicates data is acceptable.

3.4 deletion process

At this time, some friends may be thinking about it. The writing process is theoretically not too different from deletion. Why the performance of sync.Map type deletion seems to be OK, what's wrong with it?

The source code is as follows

Func (m * Map) LoadAndDelete (key interface {}) (value interface {}, loaded bool) {read, _: = m.read.Load (). (readOnly) e, ok: = read.m [key]. If ok {return e.delete ()}}

Deletion is the standard opening, still go to read to check whether the element exists.

If it exists, the call to delete is marked as expunged (deletion status), which is very efficient. Can be clear in the read elements, deleted, the performance is very good.

If it doesn't exist, it goes into the dirty process:

Func (m * Map) LoadAndDelete (key interface {}) (value interface {}, loaded bool) {. 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] delete (m.dirty, key) m.missLocked ()} m.mu.Unlock ()} Return nil, false}

If this element does not exist in read, dirty is not empty, and read is inconsistent with dirty (discriminated by amended), it indicates that dirty should be operated and mutex should be applied.

Double check again, if the element still does not exist in read. The deletion of the element is marked from the dirty by calling the delete method.

It is important to note that delete methods appear more frequently:

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

This method sets the entry.p to nil and marks it as expunged (deletion status) instead of actually deleting it.

Note: don't misuse sync.Map, some time ago, from the case shared by byte bosses, they put a connection as key, so the memory related to this connection, such as buffer, will never be freed.

At this point, the study on "how to use Go to read and write sync.map statements concurrently" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report