[Golang]解决Map的并发性问题:sync.Map
先说问题
Golang的原生Map是不支持并发写的,但是可以并发读,是一种非线程安全的结构。以下程序直接报错: fatal error: concurrent map read and map write,即便访问的是不同的key。
func main() {m:=make(map[int]int)m[1] = 1go func() {for {m[0] = 1}}()go func() {for {_ = m[1]}}()time.Sleep(time.Second)
}
解决方案
Golang 1.9之后的版本加入了sync.Map结构,一种并发安全的结构,源码中这样介绍
翻译过来就是(1)写一次但是读取多次,只会增加缓存的长度而已(2)多线程读写不同的键能够明显减少锁的争用。
源码解析
1、基础结构
sync.Map结构利用了两个原生map结构读写,read主要负责读,而dirty主要负责写,只需要在合适的时机进行同步即可。此外,read和dirty的map的值存的都是指针,指向都是同一份数据,所以不会有太大的内存消耗,而且修改read[key]那么dirty[key]的值也被修改了。
// sync.Map结构
type Map struct {mu Mutex// 主要负责读read atomic.Value // 可以看成是read的后备箱,主要负责写// interface{}是key的类型,*entry是value的类型dirty map[interface{}]*entry // 当计数达到len(dirty)时,同步dirty->read,nil->dirty// 【坑位1:触发计数条件】misses int
}
// Map结构成员read类型atomic.Value结构
type Value struct {v interface{}
}
// Value成员v存的便是readOnly结构
type readOnly struct {// 和dirty一样的结构,后面我直接说read代替mm map[interface{}]*entry // 如果dirty里面有read没有的[删除的不算]entry则为true,可以理解为dirty不落后于read// 毕竟优先往diryt里面写,read先放出去让用户去读amended bool
}
// map中key-value中的value的类型
type entry struct {// 可以指向正常的变量;或者等于nil,其实nil也可以看成正成变量,但是还是单独拎出来吧;或者等于expunged,一个用于标识的自定义指针变量// expunged标志作用:为了支持延迟删除【坑位2:如何支持】// expunged含义:read里面标记了该entry的删除,同时dirty不为nil且dirty中却没有存上该entry,当然key也没有存p unsafe.Pointer
}
// 一个用于标识的自定义指针变量
var expunged = unsafe.Pointer(new(interface{}))
2、常规方法:增改、删、查
**增改:**store函数。重点关注对象dirty成员;只要dirty不为nil,store函数执行完必须保证dirty不能落后于read【坑位3:为什么】。扩容只会发生在dirty上,读read不影响。
func (m *Map) Store(key, value interface{}) {// 前面说了Map里面的read成员存的就是ReadOnly结构,所以先转换过去read, _ := m.read.Load().(readOnly)// 拿到了该key对应的old_value,那就尝试把old_value覆盖掉if e, ok := read.m[key]; ok && e.tryStore(&value) {return}// read里面没有该key对应的value;或者没存上//涉及到了写的操作,需要加锁m.mu.Lock()read, _ = m.read.Load().(readOnly) // double check// read里面有该key对应的value,但是value已经被标记成expunged了(毕竟上面没return)if e, ok := read.m[key]; ok {// 将read里面标记为expunged的数据更新到dirty中,同时修改标记entry=nil,毕竟dirty里面也会有该数据了if e.unexpungeLocked() {m.dirty[key] = e}// 更新该值e.storeLocked(&value)// read里面没找到,那么本身dirty就是不落后于read的,直接更新了dirty里面该key对应的value即可} else if e, ok := m.dirty[key]; ok {e.storeLocked(&value)// read里面和dirty里面都没有,那就要新增了} else {// 首先看一下dirty是不是落后于read,落后就进入ifif !read.amended {// 用read去更新dirty,read->dirtym.dirtyLocked()// 更新一下read,数据没变,只不过dirty不再落后于它了,amended设置为true// 可以看出,dirty不为nil的时候,amended一定为true;换句话说,dirty只要不为nil,就一定不落后于readm.read.Store(readOnly{m: read.m, amended: true})}// 最后把该key,value加入到dirty;能看出来read里面是没有的m.dirty[key] = newEntry(value)}m.mu.Unlock()
}
func (e *entry) tryStore(i *interface{}) bool {for {p := atomic.LoadPointer(&e.p)// 根据前面说的expunged的含义,同时要保证store函数执行完必须保证dirty不能落后于read// 所以不能简单的更新该key对应的值,因为dirty里面还没有这份数据呢if p == expunged {return false}// 走正常更新逻辑if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {return true}}
}
func (e *entry) unexpungeLocked() (wasExpunged bool) {return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// 将read里面那些没意义的(值为nil)和标记删除的(值为expunged)过滤掉,之后赋值给dirty
// 同时把没意义的(值为nil)值修改为标记删除(expunged),毕竟diryt里面没有它
// 【这里注意下:nil是可以成为我们的一个普通值的,而expunged是我们自定义标识】
func (m *Map) dirtyLocked() {if m.dirty != nil {return}read, _ := m.read.Load().(readOnly)m.dirty = make(map[interface{}]*entry, len(read.m))//彻底删除还需要达到misses计数,触发missLocked函数for k, e := range read.m {if !e.tryExpungeLocked() {m.dirty[k] = e}}
}
//
func (e *entry) tryExpungeLocked() (isExpunged bool) {p := atomic.LoadPointer(&e.p)for p == nil {if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {return true}p = atomic.LoadPointer(&e.p)}return p == expunged
}
删: delete函数。
func (m *Map) Delete(key interface{}) {m.LoadAndDelete(key)
}
// 直接看这个就好
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {read, _ := m.read.Load().(readOnly)e, ok := read.m[key]// read里没找到,而且dirty是不落后于read的if !ok && read.amended {m.mu.Lock()read, _ = m.read.Load().(readOnly) // double checke, ok = read.m[key]if !ok && read.amended {e, ok = m.dirty[key]// 直接在dirty里面对这个key删除即可,read根本无感知delete(m.dirty, key)// 【填坑1】read中找不到,且dirty不落后于read,无论在dirty找没找到都会使集数加1m.missLocked()}m.mu.Unlock()}if ok {return e.delete()}return nil, false
}
func (e *entry) delete() (value interface{}, ok bool) {for {p := atomic.LoadPointer(&e.p)if p == nil || p == expunged {return nil, false}// 可以看到只有这一个地方会把value设置nil,而store函数是能保证dirty不落后于read的,if atomic.CompareAndSwapPointer(&e.p, p, nil) {return *(*interface{})(p), true}}
}
// 当read如果 落后于dirty多次,就会将dirty提升为read,这也是read的唯一来源
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
}
查:load函数。可以看出读是不会立刻加锁的
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {read, _ := m.read.Load().(readOnly)e, ok := read.m[key]// read里没查到,而且dirty是不落后于read的那就从dirty里面再查一下if !ok && read.amended {m.mu.Lock()read, _ = m.read.Load().(readOnly) // double checke, ok = read.m[key]if !ok && read.amended {e, ok = m.dirty[key]// 【填坑1】触发条件read里面没找到去不落后于read的dirty里面找就会触发m.missLocked()}m.mu.Unlock()}if !ok {return nil, false}return e.load()
}
注意的一些点可能会帮助理解:
1、结论1: value是expunged那么dirty一定不为nil。value被标记为expunged的唯一方式就是在store函数dirtyLocked()-> tryExpungeLocked(),同时会将amended设置为true,这两个操作是绑定的。所以只要有value是expunged,那么amended一定为true,也就表示dirty不落后于read的,而且可以看出expunged一定是只会出现在read里面的key对应的value上,dirty是一定不会有的,毕竟这是唯一设置expunged的位置。
2、结论2: dirty只要不为nil,那么dirty就一定不会落后于read。再查看一下dirty被新增或者删除的代码位置【置为空,修改值不算,因为都不会影响dirty是否不落后于read】。(1)store函数中在read中找到了,但是value被标记为expunged,那么在dirty中新增该key,如第一点所说expunged存在就表示dirty不落后于read。(2)store函数中read中没找到且dirty中也没有,那么就会在dirty中新增,这种方式更不会导致dirty落后于read。所以只要dirty不为nil就一定会一直不落后于read的。
3、结论3: value是nil,那么dirty是nil要么非nil(废话),说白了nil就是一个寻常的值而已,就像你可以往map存放(key,nil),在这里作为了中间态(当然,中间态设置为1,2,3…都可以,只要是个对你没有意义的变量就行)。value被标记为nil有两种方式,一种是在store函数中unexpungeLocked()中,而触发这个语句的条件是value为expunged,同时如第一点所说,dirty是不落后于read的;另一种是在delete函数中,在read中找到了该key,那么如果该key对应value非nil且非expunged,那么就会更新为nil,而此时dirty可能为nil或者不为nil,但是不为nil的时候dirty也一定会有该key的,毕竟read都是从dirty中更新过去的,所以对于value正常的read有dirty一定有。
【填坑2:延迟删除】 综上可以看出,在missLocked()中直接用dirty覆盖掉read的时候就会剔除掉了read中的expunged标识的(key,value),毕竟expunged标志只在read中的value才有。
【填坑3:为何不落后】 因为涉及到用dirty更新read的操作,那么只要dirty不为nil就一定要不落后于read才行啊。
[Golang]解决Map的并发性问题:sync.Map相关推荐
- c++ map 多线程同时更新值 崩溃_深入理解并发安全的 sync.Map
golang中内置了map关键字,但是它是非线程安全的.从go 1.9开始,标准库加入了sync.Map,提供用于并发安全的map. 普通map的并发问题 map的并发读写代码 func main() ...
- map 值为指针_Go sync.Map 并发效率为什么那么高?
点击上方蓝色"后端开发杂谈"关注我们, 专注于后端日常开发技术分享 Go sync.Map揭秘 简介: 对于熟悉 Go 语言的同学都知道, Go 自身的 map 是不支持并发读写, ...
- Go 学习笔记(67)— Go 并发安全字典 sync.Map
1. 并发不安全的 map Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 换句话说,在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是 ...
- Golang sync.Map 原理(两个map实现 读写分离、适用读多写少场景)
参考: 由浅入深聊聊Golang的sync.Map 通过对源码的逐行分析,清晰易懂 Golang sync.Map原理 通过向 sync.Map 中增删改查来介绍sync.Map的底层原理 Golan ...
- 由浅入深聊聊Golang的sync.Map
前言 今天在技术群中有小伙伴讨论并发安全的东西,其实之前就有写过map相关文章:由浅入深聊聊Golang的map.但是没有详细说明sync.Map是怎么一回事. 回想了一下,竟然脑中只剩下" ...
- golang中的sync.Map
Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写线程不安全. 下面来看下并发情况下读写 map 时会出现的问题,代码如下: package mainfunc main() {//创建一 ...
- c语言map函数k v都是int,Go语言sync.Map(在并发环境中使用的map)
Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 下面来看下并发情况下读写 map 时会出现的问题,代码如下: // 创建一个int到int的映射 m := make(m ...
- 『每周译Go』Go sync map 的内部实现
目录 引言 a. 简单介绍并发性及其在此上下文中的应用 sync.RWMutex 和 map 一起使用的问题 介绍 sync.Map a. 在哪些场景使用 sync.Map? sync.Map 实现细 ...
- 深度解密Go语言之sync.map
工作中,经常会碰到并发读写 map 而造成 panic 的情况,为什么在并发读写的时候,会 panic 呢?因为在并发读写的情况下,map 里的数据会被写乱,之后就是 Garbage in, garb ...
- Go sync.Map 看一看
偶然看见这么篇文章:一道并发和锁的golang面试题. 虽然年代久远,但也稍有兴趣. 正好最近也看到了 sync.Map,所以想试试能不能用 sync.Map 去实现上述的功能. 我还在 gayhub ...
最新文章
- java debug体系为什么不能debug到jdk里所有的代码
- 第二天,终于搞定 —— 2014.10.28
- 别以为if slse很简单——决策树
- java 自动生成文档_[原]java开发文档的自动生成方式
- PHP框架自动加载类文件原理
- python ping 连接_Python检查ping终端的方法
- “读心术”再升级,数字识别准确率可达90%
- Git(10):删除远端仓库中多余文件(**.iml/target文件)
- mysql的sql优化工具_DBA的五款最佳SQL查询优化工具,收藏了
- java 读文件 优化_Java读取文件性能优化程序代码
- Java项目—在线考试系统
- tplink控制上网设备_tp link无线路由器设置里的主人设备 访客网络
- NUBT 1480 懒惰的风纪委Elaine
- 微信小程序发布后使用本地图片不显示问题
- el-checkbox-button 出现点击任意一个导致全选
- 坐标系转换中位姿与位置
- PowerPC VxWorks BSP分析(4.3)——BSP定制
- DOCs常用命令集合cmd常用api集合
- 【数据结构与算法】——第二章:线性表
- android大字体桌面,简易桌面老人下载
热门文章
- 机器学习笔记(一):机器的学习定义、导数和最小二乘 | 凌云时刻
- 九存:重新定义存储矿机
- 开篇第一章,开宗明义。
- 【路径规划】基于matlab A_star算法机器人走迷宫路径规划【含Matlab源码 1332期】
- 【优化调度】基于matlab遗传算法求解码头泊位分配调度优化问题【含Matlab源码 247期】
- 【数字信号处理】基于matlab GUI IIR低通+FIR高通信号时域+频谱分析【含Matlab源码 1029期】
- 怎么升级Android Studio版本,Android studio 2 版本升级 Android studio 3 版本注意事项
- linux系统的电脑要不要买,是否有必要为Linux购买一台相对较高配置的电脑?
- python中的字典操作_python中的字典以及相关操作
- c语言链表插入尾部,为什么我的程序一执行插入链表尾部,再执行别的操作就会出现问题,...