c++ map 多线程同时更新值 崩溃_深入理解并发安全的 sync.Map

golang中内置了map关键字,但是它是非线程安全的。从go 1.9开始,标准库加入了sync.Map,提供用于并发安全的map。

168a7350fa46e039da6217edebf3b0ed.gif

普通map的并发问题

map的并发读写代码

func main() {  m := make(map[int]int)  go func() {    for {      _ = m[1] // 读    }  }()  go func() {    for {      m[2] = 2 // 写    }  }()  select {} // 维持主goroutine}

以上是一段并发读写map的代码, 其中一个goroutine一直读,另外一个goroutine一直写。即使读写的map键不相同,且不存在"扩容"等操作,代码还是会报错。

fatal error: concurrent map read and map write
168a7350fa46e039da6217edebf3b0ed.gif

锁+map

那普通map有没有能力实现并发安全呢?答案是肯定的。通过给map额外绑定一个锁(sync.Mutex或sync.RWMutex),封装成一个新的struct,实现并发安全。

定义带有锁的对象M

type M struct {  sync.RWMutex  Map map[int]int}

执行并发读写

func main() {  m := M{Map: make(map[int]int)}  go func() {    for {      m.RLock()      v := m.Map[2] // 读      fmt.Println(v)      m.RUnlock()    }  }()  go func() {    for i := 1; i > 0; i++ {      m.Lock()      m.Map[2] = i // 写      m.Unlock()    }  }()  select {}}

在读goroutine读数据时,通过读锁锁定,在写goroutine写数据时,写锁锁定,程序就能并发安全的运行,运行结果示意如下。

...112311241125...
168a7350fa46e039da6217edebf3b0ed.gif

sync.Map

既然通过加锁的方式就能解决map的并发问题,实现方式简洁,并且利用读写锁而不是Mutex可以进一步减少读写的时候因为锁而带来的性能损耗。那么为什么还会有sync.Map的出现?

当map的数据量非常大时,会引发并发的大量goroutine争夺同一把锁,这种现象将直接导致性能的急剧下降。在java中有类似于map的hashMap,它同样是并发不安全,但是JDK提供了线程安全的ConcurrentHashMap,它在面对上述场景时,其核心解决方案是锁分段技术,即内部使用多个锁,每个区间共享一把锁,当多线程访问map中的不同数据段的数据时,线程间就不会存在锁竞争,从而提高了并发访问效率。那sync.Map采取的是什么策略来提升并发性能的呢?

sync.Map的源码结构(基于1.14.1)

type Map struct {  // 此锁是为了保护Map中的dirty数据  mu Mutex  // 用来存读的数据,只读类型,不会造成读写冲突  read atomic.Value // readOnly  // dirty包含最新的写入数据(entry),在写的同时,将read中未删除的数据拷贝到dirty中  // 因为是go中内置的普通map类型,且涉及写操作,所以需要通过mu加锁  dirty map[interface{}]*entry  // 当读数据时,该字段不在read中,尝试从dirty中读取,不管是否在dirty中读取到数据,misses+1  // 当累计到len(dirty)时,会将dirty拷贝到read中,并将dirty清空,以此提升读性能。  misses int}

(左右滑动查看完整代码图片)

在sync.Map中用到了两个冗余数据结构read、dirty。其中read的类型为atomic.Value,它会通过atomic.Value的Load方法将其断言为readOnly对象。

read, _ := m.read.Load().(readOnly) // m为sync.Map

因此,read的真实类型即是readOnly,其数据类型如下。

type readOnly struct {  // read 中的go内置map类型,但是它不需要锁。  m       map[interface{}]*entry  // 当sync.Map.diry中的包含了某些不在m中的key时,amended的值为true.  amended bool}

(左右滑动查看完整代码图片)

amended属性的作用是指明dirty中是否有readOnly.m中未包含的数据,因此当对sync.Map的读操作在read中找不到数据时,将进一步到dirty中查找。

readOnly.m和Map.dirty中map存储的值类型是*entry,它包含一个指针p,指向用户存储的value值。

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

entry.p的值有三种类型:

  • nil:entry已经被删除,且m.dirty为nil

  • expunged:entry被删除,m.dirty不为nil,但entry不存在m.dirty中

  • 其他:entry有效,记录在m.read中,若dirty不为空,也会记录在dirty中。

虽然read和dirty存在冗余数据,但是这些数据entry是通过指针指向的,因此,尽管Map的value可能会很大,但是空间存储还是足够的。

以上是sync.Map的数据结构,下面着重看看它的四个方法实现:Load、Store、Delete和Range。

Load

加载方法,通过提供的键key,查找对应的值value。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {  // 首先从m.read中通过Load方法得到readOnly  read, _ := m.read.Load().(readOnly)  // 从read中的map中查找key  e, ok := read.m[key]  // 如果在read中没有找到,且表明有新数据在diry中(read.amended为true)  // 那么,就需要在dirty中查找,这时需要加锁。  if !ok && read.amended {    m.mu.Lock()    // 双重检查:避免在本次加锁的时候,有其他goroutine正好将Map中的dirty数据复制到了read中。    // 能发生上述可能的原因是以下两行代码语句,并不是原子操作。     // if !ok && read.amended {    //   m.mu.Lock()    // }    // 而Map.read其并发安全性的保障就在于它的修改是通过原子操作进行的。    // 因此需要再检查一次read.    read, _ = m.read.Load().(readOnly)    e, ok = read.m[key]    // 如果m.read中key还是不存在,且dirty中有新数据,则检查dirty中的数据。    if !ok && read.amended {      e, ok = m.dirty[key]      // 不管是否从dirty中得到了数据,都会将misses的计数+1      m.missLocked()    }    m.mu.Unlock()  }  if !ok {    return nil, false  }  // 通过Map的load方法,将entry.p加载为对应指针,再返回指针指向的值  return e.load()}

(左右滑动查看完整代码图片)

Map.missLocked函数是保证sync.Map性能的重要函数,它的目的是将存在有锁的dirty中的数据,转移到只读线程安全的read中去。

func (m *Map) missLocked() {  m.misses++ // 计数+1  if m.misses < len(m.dirty) {    return  }  m.read.Store(readOnly{m: m.dirty}) // 将dirty数据复制到read中去  m.dirty = nil // dirty清空  m.misses = 0 // misses重置为0}

(左右滑动查看完整代码图片)

Store

该方法更新或新增键值对key-value。

func (m *Map) Store(key, value interface{}) {  // 如果m.read中存在该键,且该键没有被标记删除(expunged)  // 则尝试直接存储(见entry的tryStore方法)  // 注意: 如果m.dirty中也有该键(key对应的entry),由于都是通过指针指向,所有m.dirty中也会保持最新entry值。  read, _ := m.read.Load().(readOnly)  if e, ok := read.m[key]; ok && e.tryStore(&value) {    return  }  // 如果不满足上述条件,即m.read不存在或者已经被标记删除  m.mu.Lock()  read, _ = m.read.Load().(readOnly)  if e, ok := read.m[key]; ok { // 如果read中有该键    if e.unexpungeLocked() { // 判断entry是否被标记删除      // 如果entry被标记删除,则将entry添加进m.dirty中      m.dirty[key] = e    }    // 更新entry指向value地址    e.storeLocked(&value)  } else if e, ok := m.dirty[key]; ok { //dirty中有该键:更新    e.storeLocked(&value)  } else { // dirty和read中均无该键:新增    if !read.amended { // 表明dirty中没有新数据,在dirty中增加第一个新键      m.dirtyLocked() // 从m.read中复制未删除的数据到dirty中      m.read.Store(readOnly{m: read.m, amended: true})    }    m.dirty[key] = newEntry(value) // 将entry增加到dirty中  }  m.mu.Unlock()}

(左右滑动查看完整代码图片)

Store的每次操作都是先从read开始,当不满足条件时,才加锁操作dirty。但是由于存在从read中复制数据的情况(例如dirty刚复制完数据给m.read,又来了一个新键),当m.read中数据量很大时,可能对性能造成影响。

Delete

删除某键值。

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()  }}// 如果read中有该键,则从read中删除,其删除方式是通过原子操作func (e *entry) delete() (hadValue bool) {  for {    p := atomic.LoadPointer(&e.p)    // 如果p指针为空,或者被标记清除    if p == nil || p == expunged {      return false    }    // 通过原子操作,将e.p标记为nil.    if atomic.CompareAndSwapPointer(&e.p, p, nil) {      return true    }  }}

(左右滑动查看完整代码图片)

Delete中的逻辑和Store逻辑相似,都是从read开始,如果这个key(也即是entry)不在read中,且dirty中有新数据,则加锁从dirty中删除。注意,和Load与Store方法一样,也是需要双检查。

Range

想要遍历sync.Map,不能通过for range的形式,因此,它自身提供了Range方法,通过回调的方式遍历。

func (m *Map) Range(f func(key, value interface{}) bool) {  read, _ := m.read.Load().(readOnly)  // 判断dirty中是否有新的数据  if read.amended {    m.mu.Lock()    // 双检查    read, _ = m.read.Load().(readOnly)    if read.amended {      // 将dirty中的数据复制到read中      read = readOnly{m: m.dirty}      m.read.Store(read)      m.dirty = nil      m.misses = 0    }    m.mu.Unlock()  }  // 遍历已经整合过dirty的read  for k, e := range read.m {    v, ok := e.load()    if !ok {      continue    }    if !f(k, v) {      break    }  }}

(左右滑动查看完整代码图片)

sync.Map的优化总结

1. 空间换时间:通过两个冗余的数据结构(read、write),减小锁对性能的影响。

2. 读操作使用read,避免读写冲突。

3. 动态调整:通过misses值,避免dirty数据过多。

4. 双检查机制:避免在非原子操作时产生数据错误。

5. 延迟删除机制:删除一个键值只是先打标记,只有等提升dirty(复制到read中,并清空自身)时才清理删除的数据。

6. 优先从read中读、改和删除,因为对read的操作不用加锁,大大提升性能。

sync.Map的使用例子

func main() {  var sm sync.Map  // 注意:同一个sync.Map,和map不一样,每个item的key或value可以和其他的数据类型不一样  // 只要满足key能hash即可  sm.Store(1, "a")  sm.Store("b", 2)  sm.Store("c", 3)  // 和map获取key值类似  if v, ok := sm.Load("b"); ok {    fmt.Println(v)  } // 删除某个key的键值对  sm.Delete(1)  // 参数fun中的参数是遍历获得的key和value,返回一个bool值  // 返回true时,继续遍历  // 返回false,遍历结束  sm.Range(func(key, value interface{}) bool {    fmt.Println(key,value)    return true  })}

(左右滑动查看完整代码图片)

输出

2b 2c 3
168a7350fa46e039da6217edebf3b0ed.gif

sync.Map的性能

在Go源码$GOROOT/src/sync中,提供了测试代码。

  • map_reference_test.go:  定义了测试用的mapInterface接口,sync.Map、RwMutexMap和DeepCopyMap对象实现该接口方法。

  • map_test.go: 三个对象的方法测试代码。

  • map_bench_test.go:  三个对象的benchmark性能对比测试代码。

在小菜刀的机器上,运行性能测试结果如下。

$ go test -bench=LoadMostlyHits -benchmemBenchmarkLoadMostlyHits/*sync_test.DeepCopyMap-8           80252629          13.5 ns/op         7 B/op         0 allocs/opBenchmarkLoadMostlyHits/*sync_test.RWMutexMap-8            23025050          51.8 ns/op         7 B/op         0 allocs/opBenchmarkLoadMostlyHits/*sync.Map-8                        67718686          14.9 ns/op         7 B/op         0 allocs/op$ go test -bench=LoadMostlyMisses -benchmemBenchmarkLoadMostlyMisses/*sync_test.DeepCopyMap-8           128480215          11.2 ns/op         7 B/op         0 allocs/opBenchmarkLoadMostlyMisses/*sync_test.RWMutexMap-8            23989224          47.4 ns/op         7 B/op         0 allocs/opBenchmarkLoadMostlyMisses/*sync.Map-8                        132403878           9.30 ns/op         7 B/op         0 allocs/op$ go test -bench=LoadOrStoreBalanced -benchmemBenchmarkLoadOrStoreBalanced/*sync_test.RWMutexMap-8             3909409         553 ns/op        99 B/op         2 allocs/opBenchmarkLoadOrStoreBalanced/*sync.Map-8                         3574923         368 ns/op        97 B/op         3 allocs/op$ go test -bench=LoadOrStoreUnique -benchmemBenchmarkLoadOrStoreUnique/*sync_test.RWMutexMap-8             2053806         647 ns/op       174 B/op         2 allocs/opBenchmarkLoadOrStoreUnique/*sync.Map-8                         2456720         577 ns/op       140 B/op         4 allocs/op$ go test -bench=LoadOrStoreCollision -benchmemBenchmarkLoadOrStoreCollision/*sync_test.DeepCopyMap-8           153679003           8.18 ns/op         0 B/op         0 allocs/opBenchmarkLoadOrStoreCollision/*sync_test.RWMutexMap-8            13718534          87.9 ns/op         0 B/op         0 allocs/opBenchmarkLoadOrStoreCollision/*sync.Map-8                        175620835           7.08 ns/op         0 B/op         0 allocs/op$ go test -bench=Range -benchmemBenchmarkRange/*sync_test.DeepCopyMap-8             416906        2947 ns/op         0 B/op         0 allocs/opBenchmarkRange/*sync_test.RWMutexMap-8               22784       52370 ns/op     16384 B/op         1 allocs/opBenchmarkRange/*sync.Map-8                          369955        3194 ns/op         0 B/op         0 allocs/op$ go test -bench=AdversarialAlloc -benchmemBenchmarkAdversarialAlloc/*sync_test.DeepCopyMap-8            1875109         646 ns/op       539 B/op         1 allocs/opBenchmarkAdversarialAlloc/*sync_test.RWMutexMap-8            19454866          61.6 ns/op         8 B/op         1 allocs/opBenchmarkAdversarialAlloc/*sync.Map-8                         3712470         320 ns/op        51 B/op         1 allocs/op$ go test -bench=AdversarialDelete -benchmemBenchmarkAdversarialDelete/*sync_test.DeepCopyMap-8            6749067         215 ns/op       168 B/op         1 allocs/opBenchmarkAdversarialDelete/*sync_test.RWMutexMap-8            16046545          76.9 ns/op        25 B/op         1 allocs/opBenchmarkAdversarialDelete/*sync.Map-8                        18678104          64.2 ns/op        18 B/op         1 allocs/op

(左右滑动查看完整代码图片)

168a7350fa46e039da6217edebf3b0ed.gif

如何选择Map

从性能测试结果可以看出,sync.Map并不是为了代替锁+map的组合。它的设计,是为了在某些并发场景下,相对前者能有较小的性能损耗。

源码文档中($GOROOT/src/sync/map.go)已经给出了sync.Map的合适场景。

// The Map type is specialized. Most code should use a plain Go map instead,// with separate locking or coordination, for better type safety and to make it// easier to maintain other invariants along with the map content.//// The Map type is optimized for two common use cases: (1) when the entry for a given// key is only ever written once but read many times, as in caches that only grow,// or (2) when multiple goroutines read, write, and overwrite entries for disjoint// sets of keys. In these two cases, use of a Map may significantly reduce lock// contention compared to a Go map paired with a separate Mutex or RWMutex.

(左右滑动查看完整代码图片)

两种情况应该选择sync.Map

  1. key值一次写入,多次读取(即写少读多场景)。

  2. 多个goroutine的读取、写入和覆盖在不相交的key集。

推荐阅读

  • 并发安全的 map:sync.Map源码分析


喜欢本文的朋友,欢迎关注“Go语言中文网”:

2ec68fc8651a6af7d99c59a06dc6dc6e.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/368026.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java 递归从子节点删除父节点_LeetCode450. 删除二叉搜索树中的节点

删除一个二叉搜索树中的节点&#xff0c;需要进行情况的分类讨论&#xff0c;看一下将这个节点删除之后是否需要对二叉搜索树进行调整&#xff08;为了保持树的连接和维持二叉搜索树的性质&#xff09;。&#xff08;1&#xff09;如果删除的是一个叶子节点&#xff0c;那问题不…

1. [文件]- 文件类型,文件open模式

1.文件类型&#xff1a;文本文件和二进制文件 硬盘中的文件保存为01010101格式&#xff0c;一般读取文件是把文件从硬盘中读取到内存中。 文本文件需要进行格式转换才能读取出来。二进制文件一般用于传输二进制文件&#xff1a;视频图片 2.文件打开模式 几种不同的读取和遍历文…

node安装node-sass失败,配置淘宝源

node-sass 安装失败的原因是因为无法下载 .node 文件&#xff0c;解决办法就很简单了&#xff0c;就是我们把文件下载路径复制一份到浏览器里&#xff0c;然后使用浏览器下载文件就可以了。 具体方法 1.从node命令行中复制 .node文件下载链接并在浏览器打开下载文件https:/…

带有Atomikos示例的Tomcat中的Spring JTA多个资源事务

在本教程中&#xff0c;我们将向您展示如何使用Atomikos Transaction Manager在Tomcat服务器中实现JTA多个资源事务。 Atomicos事务管理器为分布式事务提供支持。 这些是多阶段事务&#xff0c;通常使用多个数据库&#xff0c;必须以协调的方式提交。 分布式事务由XA standard描…

mac vs 返回上一步_mac电脑打不开应用程序的解决方法

mac电脑跟windows电脑一样&#xff0c;经常会出现打不开应用程序的情况&#xff0c;并且提示“因为它来自身份不明的开发者”&#xff0c;也不知道哪里出现问题&#xff1f;由于MAC系统与windows界面不一样&#xff0c;很多小编不懂怎么操作&#xff1f;为此&#xff0c;小编给…

iOS-----------关于组件化

打一个比较形象的比喻&#xff0c;把APP比作我们的人体&#xff0c;把胳膊、大腿、心、肝、肺这些人体器官比作组件&#xff0c;各个器官分别负责他们各自的功能&#xff0c;但是他们之间也有主次之分&#xff0c;试想我们的胳膊、大腿等是不能独立完成某个任务的&#xff0c;必…

android 自定义switch控件,Android中switch自定义样式

android 原生开关按钮控件 Switch 提供样式自定义方式&#xff0c;可供我们修改为适合我们开发使用的样式控件&#xff0c;自定义样式过程如下:自定义switch切换drawable新建swith_thumb.xml文件自定义switch轨道drawable新建switch_track.xmln文件,轨迹如果在选中与否过程并没…

具有瞬态属性的视图对象的钝化和激活

在应用程序模块的钝化/激活周期内&#xff0c;框架也会钝化并激活视图对象。 通常&#xff0c;框架保存有关VO状态&#xff0c;当前行&#xff0c;绑定变量值等的信息。 但是没有数据。 激活视图对象后&#xff0c;将重新执行VO的查询&#xff0c;并重新获取数据。 在大多数情况…

pre标签的样式

你可能正在使用 <pre> 标签。这是一个 HTML 中非常特别的标签&#xff0c;它允许其中的空格真正显示出来。例如&#xff1a;四个空格将真实显示成四个空格。这不同于其他标签通常的做法&#xff0c;其他标签会将之间的空白压缩成一个。从这一点来说&#xff0c;<pre&g…

js的闭包

function a(){var n 0;this.inc function () {n; console.log(n);}; } var c new a(); c.inc(); //控制台输出1 c.inc(); //控制台输出2 什么是闭包&#xff1f;这就是闭包&#xff01;&#xff01;有权访问另一个函数作用域内变量的函数都是闭包。当函数可以记住并访…

远程管理口怎么看地址_红烧羊肉怎么样做才能滋味浓郁,咸甜适口,且回味有奶香?看这里...

原汁原味红烧羊肉此菜在制作上不同于其他红烧羊肉时要放入香料去膻&#xff0c;但在选料上很讲究&#xff0c;也就是说食材的好坏决定菜的好坏。选用一年生的崇明母山羊制作&#xff0c;膻味很小&#xff0c;肉质软嫩细腻&#xff0c;且带有一股淡淡奶香&#xff0c;因此不必放…

css段落文字(中英文混杂)实现两端对齐

案例如下&#xff1a; 混合使用汉字和英文的段落默认如下&#xff1a; 两边是不对齐的(一般情况下&#xff0c;我们对这种情况不做处理&#xff0c;除非需求或者设计非要我们实现两端对齐)。 对齐之后如下&#xff1a; 实现思路 一般的两端对齐是使用text-align:justify&…

44集合:蒜头军学英语

转载于:https://www.cnblogs.com/passion-sky/p/8424769.html

android病毒下载地址,LINE病毒查杀

LINE病毒查杀是免费通话、免费传讯「LINE」的周边应用程序之一。它能保护智能手机上个人信息的安全&#xff0c;使其免于病毒或恶意程序的侵害。您只要执行几个简单的步骤就能确认手机状态或完成病毒扫描。LINE病毒查杀界面LINE病毒查杀软件功能1、智能手机上的病毒将无所遁形!…

android app的签名,Android APP的签名

Android APP的签名Android项目以它的包名作为唯一的标识&#xff0c;如果在同一部手机上安装两个包名相同的APP&#xff0c;后者就会覆盖前面安装的应用。为了避免Android APP被随意覆盖&#xff0c;Android要求对APP进行签名。下面介绍对APP进行签名的步骤1、选择builder菜单下…

5.6.50 mysql 用什么驱动_日均5亿查询量的京东订单中心,为什么舍弃MySQL用ES?

京东到家订单中心系统业务中&#xff0c;无论是外部商家的订单生产&#xff0c;或是内部上下游系统的依赖&#xff0c;订单查询的调用量都非常大&#xff0c;造成了订单数据读多写少的情况。我们把订单数据存储在MySQL中&#xff0c;但显然只通过DB来支撑大量的查询是不可取的。…

可搜索的文件? 是的你可以。 选择AsciiDoc的另一个原因

Elasticsearch是一个基于Apache Lucene的灵活&#xff0c;功能强大的开源&#xff0c;分布式实时云搜索和分析引擎&#xff0c;可提供全文搜索功能。 它是面向文档且无架构的。 Asciidoctor是一个纯Ruby处理器&#xff0c;用于将AsciiDoc源文件和字符串转换为HTML 5 &#xff…

Jquery 获取table当前行内容

$("a[namecheckOriginal]").click(function () { var parent $(this).parent().parent().find("td"); var moduleEnum parent.eq(7).text(); if(moduleEnum""){ } alert(moduleEnmu);}); 转载于:https://www.cnblogs.com/austi…

监视和检测Java应用程序中的内存泄漏

因此&#xff0c;您的应用程序内存不足&#xff0c;您日夜不停地分析应用程序&#xff0c;以期捕获对象中的内存漏洞。 后续步骤将说明如何监视和检测您的内存泄漏&#xff0c;以确保您的应用程序安全。 1.怀疑内存泄漏 如果您怀疑有内存泄漏&#xff0c;可以使用一种方便的方…

点a链接写邮件小技巧

无意间发现这个技巧&#xff0c;分享一下&#xff01; 当点击mailto的邮件链接的时候&#xff0c;需要填写标题和内容&#xff0c;如果你想规定一个邮件标题格式&#xff0c;那这个可以帮助你。 代码&#xff1a; <a href"mailto:haozidaqianduan.com?subject投稿&a…