Go 1.9 sync.Map揭秘
Go 1.9 sync.Map揭秘
目录 [−]
在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后, 并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。
本文带你深入到sync.Map
的具体实现中,看看为了增加一个功能,代码是如何变的复杂的,以及作者在实现sync.Map
的一些思想。
有并发问题的map
官方的faq已经提到内建的map
不是线程(goroutine)安全的。
首先,让我们看一段并发读写的代码,下列程序中一个goroutine一直读,一个goroutine一只写同一个键值,即即使读写的键不相同,而且map也没有"扩容"等操作,代码还是会报错。
|
|
错误信息是: fatal error: concurrent map read and map write
。
如果你查看Go的源代码: hashmap_fast.go#L118,会看到读的时候会检查hashWriting
标志, 如果有这个标志,就会报并发错误。
写的时候会设置这个标志: hashmap.go#L542
|
|
hashmap.go#L628设置完之后会取消这个标记。
当然,代码中还有好几处并发读写的检查, 比如写的时候也会检查是不是有并发的写,删除键的时候类似写,遍历的时候并发读写问题等。
有时候,map的并发问题不是那么容易被发现, 你可以利用-race
参数来检查。
Go 1.9之前的解决方案
但是,很多时候,我们会并发地使用map对象,尤其是在一定规模的项目中,map总会保存goroutine共享的数据。在Go官方blog的Go maps in action一文中,提供了一种简便的解决方案。
|
|
它使用嵌入struct为map增加一个读写锁。
读数据的时候很方便的加锁:
|
|
写数据的时候:
|
|
sync.Map
可以说,上面的解决方案相当简洁,并且利用读写锁而不是Mutex可以进一步减少读写的时候因为锁带来的性能。
但是,它在一些场景下也有问题,如果熟悉Java的同学,可以对比一下java的ConcurrentHashMap
的实现,在map的数据非常大的情况下,一把锁会导致大并发的客户端共争一把锁,Java的解决方案是shard
, 内部使用多个锁,每个区间共享一把锁,这样减少了数据共享一把锁带来的性能影响,orcaman提供了这个思路的一个实现: concurrent-map,他也询问了Go相关的开发人员是否在Go中也实现这种方案,由于实现的复杂性,答案是Yes, we considered it.
,但是除非有特别的性能提升和应用场景,否则没有进一步的开发消息。
那么,在Go 1.9中sync.Map
是怎么实现的呢?它是如何解决并发提升性能的呢?
sync.Map
的实现有几个优化点,这里先列出来,我们后面慢慢分析。
- 空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
- 使用只读数据(read),避免读写冲突。
- 动态调整,miss次数多了之后,将dirty数据提升为read。
- double-checking。
- 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
- 优先从read读取、更新、删除,因为对read的读取不需要锁。
下面我们介绍sync.Map
的重点代码,以便理解它的实现思想。
首先,我们看一下sync.Map
的数据结构:
|
|
它的数据结构很简单,值包含四个字段:read
、mu
、dirty
、misses
。
它使用了冗余的数据结构read
、dirty
。dirty
中会包含read
中为删除的entries,新增加的entries会加入到dirty
中。
read
的数据结构是:
|
|
amended
指明Map.dirty
中有readOnly.m
未包含的数据,所以如果从Map.read
找不到数据的话,还要进一步到Map.dirty
中查找。
对Map.read的修改是通过原子操作进行的。
虽然read
和dirty
有冗余数据,但这些数据是通过指针指向同一个数据,所以尽管Map的value会很大,但是冗余的空间占用还是有限的。
readOnly.m
和Map.dirty
存储的值类型是*entry
,它包含一个指针p, 指向用户存储的value值。
|
|
p有三种值:
- nil: entry已被删除了,并且m.dirty为nil
- expunged: entry已被删除了,并且m.dirty不为nil,而且这个entry不存在于m.dirty中
- 其它: entry是一个正常的值
以上是sync.Map
的数据结构,下面我们重点看看Load
、Store
、Delete
、Range
这四个方法,其它辅助方法可以参考这四个方法来理解。
Load
加载方法,也就是提供一个键key
,查找对应的值value
,如果不存在,通过ok
反映:
|
|
这里有两个值的关注的地方。一个是首先从m.read
中加载,不存在的情况下,并且m.dirty
中有新数据,加锁,然后从m.dirty
中加载。
二是这里使用了双检查的处理,因为在下面的两个语句中,这两行语句并不是一个原子操作。
|
|
虽然第一句执行的时候条件满足,但是在加锁之前,m.dirty
可能被提升为m.read
,所以加锁后还得再检查m.read
,后续的方法中都使用了这个方法。
双检查的技术Java程序员非常熟悉了,单例模式的实现之一就是利用双检查的技术。
可以看到,如果我们查询的键值正好存在于m.read
中,无须加锁,直接返回,理论上性能优异。即使不存在于m.read
中,经过miss
几次之后,m.dirty
会被提升为m.read
,又会从m.read
中查找。所以对于更新/增加较少,加载存在的key很多的case,性能基本和无锁的map类似。
下面看看m.dirty
是如何被提升的。 missLocked
方法中可能会将m.dirty
提升。
|
|
上面的最后三行代码就是提升m.dirty
的,很简单的将m.dirty
作为readOnly
的m
字段,原子更新m.read
。提升后m.dirty
、m.misses
重置, 并且m.read.amended
为false。
Store
这个方法是更新或者新增一个entry。
|
|
你可以看到,以上操作都是先从操作m.read
开始的,不满足条件再加锁,然后操作m.dirty
。
Store
可能会在某种情况下(初始化或者m.dirty刚被提升后)从m.read
中复制数据,如果这个时候m.read
中数据量非常大,可能会影响性能。
Delete
删除一个键值。
|
|
同样,删除操作还是从m.read
中开始, 如果这个entry不存在于m.read
中,并且m.dirty
中有新数据,则加锁尝试从m.dirty
中删除。
注意,还是要双检查的。 从m.dirty
中直接删除即可,就当它没存在过,但是如果是从m.read
中删除,并不会直接删除,而是打标记:
|
|
Range
因为for ... range map
是内建的语言特性,所以没有办法使用for range
遍历sync.Map
, 但是可以使用它的Range
方法,通过回调的方式遍历。
|
|
Range方法调用前可能会做一个m.dirty
的提升,不过提升m.dirty
不是一个耗时的操作。
sync.Map的性能
Go 1.9源代码中提供了性能的测试: map_bench_test.go、map_reference_test.go
我也基于这些代码修改了一下,得到下面的测试数据,相比较以前的解决方案,性能多少回有些提升,如果你特别关注性能,可以考虑sync.Map
。
|
|
其它
sync.Map
没有Len
方法,并且目前没有迹象要加上 (issue#20680),所以如果想得到当前Map中有效的entries的数量,需要使用Range
方法遍历一次, 比较X疼。
LoadOrStore
方法如果提供的key存在,则返回已存在的值(Load),否则保存提供的键值(Store)。
Go 1.9 sync.Map揭秘的更多相关文章
- go的sync.Map
sync.Map这个数据结构是线程安全的(基本类型Map结构体在并发读写时会panic严重错误),它填补了Map线程不安全的缺陷,不过最好只在需要的情况下使用.它一般用于并发模型中对同一类map结构体 ...
- 深入理解golang:sync.map
疑惑开篇 有了map为什么还要搞个sync.map 呢?它们之间有什么区别? 答:重要的一点是,map并发不是安全的. 在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没 ...
- Golang:sync.Map
由于map在gorountine 上不是安全的,所以在大量并发读写的时候,会出现错误. 在1.9版的时候golang推出了sync.Map. sync.Map 通过阅读源码我们发现sync.Map是通 ...
- sync.Map(在并发环境中使用的map)
sync.Map 有以下特性: 需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是 ...
- sync.Map与Concurrent Map
1. sync.Map 1.1. map并发不安全 go1.6以后map有了并发的安全检查,所以如果在并发环境中读写map就会报错 func unsafeMap() { // 创建一个map对象 m ...
- golang 标准库 sync.Map 中 nil 和 expunge 区别
本文不是 sync.Map 源码详细解读,而是聚焦 entry 的不同状态,特别是 nil 状态和 expunge 状态的区分. entry 是 sync.Map 存放值的结构体,其值有三种,分别为 ...
- 图解Go里面的sync.Map了解编程语言核心实现源码
基础筑基 在大多数语言中原始map都不是一个线程安全的数据结构,那如果要在多个线程或者goroutine中对线程进行更改就需要加锁,除了加1个大锁,不同的语言还有不同的优化方式, 像在java和go这 ...
- 源码解读 Golang 的 sync.Map 实现原理
简介 Go 的内建 map 是不支持并发写操作的,原因是 map 写操作不是并发安全的,当你尝试多个 Goroutine 操作同一个 map,会产生报错:fatal error: concurrent ...
- 深度解密 Go 语言之 sync.map
工作中,经常会碰到并发读写 map 而造成 panic 的情况,为什么在并发读写的时候,会 panic 呢?因为在并发读写的情况下,map 里的数据会被写乱,之后就是 Garbage in, garb ...
随机推荐
- webapi从入门到放弃(一)OWIN 自寄宿模式
1.创建web空项目 2.创建完如图 3.安装如下程序包Microsoft.AspNet.WebApi.Core (5.2.4)Microsoft.Owin.Host.SystemWeb (4.0. ...
- 为何90%的IT技术人员不适合做老大
老男孩老师终于写了一篇让我觉得不错的文章,哈哈,转一下. 什么是格局? 格局就是能够很好的平衡短期利益和长期利益. 过分注重短期利益的人必然会失去长期利益,到头来一定会很普通. 例如:跳槽不断,可能短 ...
- AngularJs 学习笔记(一)作用域
AngularJs采用了注重时效的MVC方式,是基于MVW模式. 1.$scope和作用域的概念. AngularJs中的$scope对象是模板的域模型,也称作作用域实例,通过为其属性赋值,可以传递给 ...
- windows10上pip install channels
之前一直在MBP上做开发,在windows偶尔改一次代码,最近在windows上Pipi nstall了一次Django Channels,其中到twisted那步出现数坑 1. Microsoft ...
- TensorFlow图像处理API
TensorFlow提供了一些常用的图像处理接口,可以让我们方便的对图像数据进行操作,以下首先给出一段显示原始图片的代码,然后在此基础上,实践TensorFlow的不同API. 显示原始图片 impo ...
- hadoop HA 详解
NameNode 高可用整体架构概述 在 Hadoop 1.0 时代,Hadoop 的两大核心组件 HDFS NameNode 和 JobTracker 都存在着单点问题,这其中以 NameNode ...
- AngularJS 最常用的几种功能
AngularJS 最常用的几种功能 2017-04-13 吐槽阿福 互联网吐槽大会 第一 迭代输出之ng-repeat标签ng-repeat让table ul ol等标签和js里的数组完美结合 1 ...
- Spark Streaming入门
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文将帮助您使用基于HBase的Apache Spark Streaming.Spark Streaming是Spark API核心的一个扩 ...
- MYSQL使用方法
显示所有数据库:show databases; 创建数据库:create database 数据库名; 删除数据库:drop database 数据库名: 查看表结构: describe(desc ...
- ucloud mysql
[root@--- bin]# yum install mysql-server Loaded plugins: fastestmirror Setting up Install Process Lo ...