先说问题

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相关推荐

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

    golang中内置了map关键字,但是它是非线程安全的.从go 1.9开始,标准库加入了sync.Map,提供用于并发安全的map. 普通map的并发问题 map的并发读写代码 func main() ...

  2. map 值为指针_Go sync.Map 并发效率为什么那么高?

    点击上方蓝色"后端开发杂谈"关注我们, 专注于后端日常开发技术分享 Go sync.Map揭秘 简介: 对于熟悉 Go 语言的同学都知道, Go 自身的 map 是不支持并发读写, ...

  3. Go 学习笔记(67)— Go 并发安全字典 sync.Map

    1. 并发不安全的 map Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 换句话说,在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是 ...

  4. Golang sync.Map 原理(两个map实现 读写分离、适用读多写少场景)

    参考: 由浅入深聊聊Golang的sync.Map 通过对源码的逐行分析,清晰易懂 Golang sync.Map原理 通过向 sync.Map 中增删改查来介绍sync.Map的底层原理 Golan ...

  5. 由浅入深聊聊Golang的sync.Map

    前言 今天在技术群中有小伙伴讨论并发安全的东西,其实之前就有写过map相关文章:由浅入深聊聊Golang的map.但是没有详细说明sync.Map是怎么一回事. 回想了一下,竟然脑中只剩下" ...

  6. golang中的sync.Map

    Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写线程不安全. 下面来看下并发情况下读写 map 时会出现的问题,代码如下: package mainfunc main() {//创建一 ...

  7. c语言map函数k v都是int,Go语言sync.Map(在并发环境中使用的map)

    Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 下面来看下并发情况下读写 map 时会出现的问题,代码如下: // 创建一个int到int的映射 m := make(m ...

  8. 『每周译Go』Go sync map 的内部实现

    目录 引言 a. 简单介绍并发性及其在此上下文中的应用 sync.RWMutex 和 map 一起使用的问题 介绍 sync.Map a. 在哪些场景使用 sync.Map? sync.Map 实现细 ...

  9. 深度解密Go语言之sync.map

    工作中,经常会碰到并发读写 map 而造成 panic 的情况,为什么在并发读写的时候,会 panic 呢?因为在并发读写的情况下,map 里的数据会被写乱,之后就是 Garbage in, garb ...

  10. Go sync.Map 看一看

    偶然看见这么篇文章:一道并发和锁的golang面试题. 虽然年代久远,但也稍有兴趣. 正好最近也看到了 sync.Map,所以想试试能不能用 sync.Map 去实现上述的功能. 我还在 gayhub ...

最新文章

  1. java debug体系为什么不能debug到jdk里所有的代码
  2. 第二天,终于搞定 —— 2014.10.28
  3. 别以为if slse很简单——决策树
  4. java 自动生成文档_[原]java开发文档的自动生成方式
  5. PHP框架自动加载类文件原理
  6. python ping 连接_Python检查ping终端的方法
  7. “读心术”再升级,数字识别准确率可达90%
  8. Git(10):删除远端仓库中多余文件(**.iml/target文件)
  9. mysql的sql优化工具_DBA的五款最佳SQL查询优化工具,收藏了
  10. java 读文件 优化_Java读取文件性能优化程序代码
  11. Java项目—在线考试系统
  12. tplink控制上网设备_tp link无线路由器设置里的主人设备 访客网络
  13. NUBT 1480 懒惰的风纪委Elaine
  14. 微信小程序发布后使用本地图片不显示问题
  15. el-checkbox-button 出现点击任意一个导致全选
  16. 坐标系转换中位姿与位置
  17. PowerPC VxWorks BSP分析(4.3)——BSP定制
  18. DOCs常用命令集合cmd常用api集合
  19. 【数据结构与算法】——第二章:线性表
  20. android大字体桌面,简易桌面老人下载

热门文章

  1. 机器学习笔记(一):机器的学习定义、导数和最小二乘 | 凌云时刻
  2. 九存:重新定义存储矿机
  3. 开篇第一章,开宗明义。
  4. 【路径规划】基于matlab A_star算法机器人走迷宫路径规划【含Matlab源码 1332期】
  5. 【优化调度】基于matlab遗传算法求解码头泊位分配调度优化问题【含Matlab源码 247期】
  6. 【数字信号处理】基于matlab GUI IIR低通+FIR高通信号时域+频谱分析【含Matlab源码 1029期】
  7. 怎么升级Android Studio版本,Android studio 2 版本升级 Android studio 3 版本注意事项
  8. linux系统的电脑要不要买,是否有必要为Linux购买一台相对较高配置的电脑?
  9. python中的字典操作_python中的字典以及相关操作
  10. c语言链表插入尾部,为什么我的程序一执行插入链表尾部,再执行别的操作就会出现问题,...