在 golang中,想要并发安全的操作map,可以使用sync.Map结构,sync.Map 是一个适合读多写少的数据结构,今天我们来看看它的设计思想,来看看为什么说它适合读多写少的场景。

如下,是golang 中sync.Map的数据结构,其中 属性read 是 只读的 map,dirty 是负责写入的map,sync.Map中的键值对value值本质上都是entry指针类型,entry中的p才指向了实际存储的value值

// sync.Map的核心数据结构
type Map struct {
mu Mutex // 对 dirty 加锁保护,线程安全
read atomic.Value // read 只读的 map,充当缓存层
dirty map[interface{}]*entry // 负责写操作的 map,当misses = len(dirty)时,将其赋值给read
misses int // 未命中 read 时的累加计数,每次+1
}
// 上面read字段的数据结构
type readOnly struct {
m map[interface{}]*entry //
amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
} // 上面m字段中的entry类型
type entry struct {
// value是个指针类型
p unsafe.Pointer // *interface{}
}

我们从一个sync.Map的数据写入和数据查询 两个过程来分析这两个map中数据的变化。

我将不展示具体的代码,仅仅讲述数据的流动,相信懂了这个以后再去看代码应该不难。

步骤一: 首先是一个初始的sync.Map 结构,我们往其中写入数据,数据会写到dirty中,同时,由于sync.Map 刚刚创建,所以read map还不存在,所以这里会先初始化一个read map 。amended 是read map中的一个属性,为true代表 dirty 和read中数据不一致。

步骤二: 接着,如果后续再继续写入新数据,

在read map没有从dirty 同步数据之前,即amended 变为false之前,再写入新键值对都只会往dirty里写。

步骤三: 如果有读操作,sync.Map 都会尽可能的让其先读read map,read map读取不到并且amended 为true,即read 和dirty 数据不一致时,会去读dirty,读dirty的过程是上锁的。

步骤四: 当读取read map中miss次数大于等于dirty数组的长度时,会触发dirty map整体更新为readOnly map,并且这个过程是阻塞的。更新完成后,原先dirty会被置为空,amended 为false,代表read map同步了之前所有的数据。如下图所示,

整体更新的逻辑是直接替换变量的值,并非挨个复制,

func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
} // 将dirty置给read,因为穿透概率太大了(原子操作,耗时很小)
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}

步骤五: 如果后续sync.Map 不再插入新数据,那么读取时就可以一直读取read map中的数据了,直接读取read map 中的key是十分高效的,只需要用atomic.Load 操作 取到readOnly map结构体,然后从中取出特定的key就行。

如果读miss了,因为没有插入新数据,read.amended=false 代表read 是保存了所有的k,v键值对,读miss后,也不会再去读取dirty了,也就不会有读dirty加锁的过程。

// 上面read字段的数据结构
type readOnly struct {
m map[interface{}]*entry //
amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
} func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 因read只读,线程安全,优先读取
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key] // 如果read没有,并且dirty有新数据,那么去dirty中查找(read.amended=true:dirty和read数据不一致)
// 暂时省略 后续代码
....... }

上面的获取key对应的value过程甚至比RWMutex 读锁下获取map中的value还要高效,毕竟RWmutex 读取时还需要加上读锁,其底层是用atomic.AddInt32 操作,而sync.Map 则是用 atomic.load 获取map,atomic.AddInt32 的开销比atomic.load 的开销要大。

,所以,为什么我们说golang的sync.Map 在大量读的情况下性能极佳,因为在整个读取过程中没有锁开销,atomic.load 原子操作消耗极低。

但是如果后续又写入了新的键值对数据,那么 dirty map中就会又插入到新的键值对,dirty和read的数据又不一致了,read 的amended 将改为true。

并且由于之前dirty整体更新为read后,dirty字段置为nil了,所以,在更改amended时,也会将read中的所有未被删除的key同步到 dirty中

注意,为什么在dirty整体更新一次read map后,再写入新的键值对时,需要将read map中的数据全部同步到dirty,因为随着dirty的慢慢写入,后续读操作又会造成读miss的增加,最终会再次触发dirty map整体更新为readOnly map,amended 改为false,代表read map中又有所有键值对数据了,也就是会回到步骤三的操作,重复步骤三到步骤五的过程。

只有将read map中的数据全部同步到dirty ,才能保证后续的整体更新,不会造成丢失数据。

看到这里应该能够明白sync.Map的适合场景了,我来总结下,

sync.Map 适合读多写少的场景,大量的读操作可以通过只读取read map 拥有极好的性能。

而如果写操作增加,首先会造成read map中读取miss增加,会回源到dirty中读取,且dirty可能会频繁整体更新为read,回源读取,整体更新的步骤都是阻塞上锁的。

其次,写操作也会带来dirty和 read中数据频繁的不一致,导致read中的数据需要同步到dirty中,这个过程在键值对比较多时,性能损耗较大且整个过程是阻塞的。

所以sync.Map 并不适合大量写操作。

golang sync.Map之如何设计一个并发安全的读写结构?的更多相关文章

  1. Golang:sync.Map

    由于map在gorountine 上不是安全的,所以在大量并发读写的时候,会出现错误. 在1.9版的时候golang推出了sync.Map. sync.Map 通过阅读源码我们发现sync.Map是通 ...

  2. 深入理解golang:sync.map

    疑惑开篇 有了map为什么还要搞个sync.map 呢?它们之间有什么区别? 答:重要的一点是,map并发不是安全的. 在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没 ...

  3. sync.Map(在并发环境中使用的map)

    sync.Map 有以下特性: 需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是 ...

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

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

  5. concurrent-map 和 sync.Map,我该选择哪个?

    concurrent-map 和 sync.Map,我该选择哪个? 官方的map并不是线程安全的,如果我们在多线程中并发对一个map进行读写操作,是会引发panic的.解决方案除了使用锁来对map进行 ...

  6. sync.Map与Concurrent Map

    1. sync.Map 1.1. map并发不安全 go1.6以后map有了并发的安全检查,所以如果在并发环境中读写map就会报错 func unsafeMap() { // 创建一个map对象 m ...

  7. GO 语言中的 sync Map

    为什么需要 sync map go 语言之所以引入 sync.Map主要是因为GO 语言自带的 map 是线程不安全的.只能保证并发的读,但是不能保证并发的写. 看下面的例子: func main() ...

  8. 并发安全 sync.Map

    https://mp.weixin.qq.com/s/MqPm7QH3_D9roVkpTs9Xpw 谈谈Go的并发安全相关 原创 歪鼻子 歪鼻子 2020-12-27     package main ...

  9. golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题

    golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题 下面这个程序运行的能num结果是什么? package main import ( "fmt" " ...

  10. 源码解读 Golang 的 sync.Map 实现原理

    简介 Go 的内建 map 是不支持并发写操作的,原因是 map 写操作不是并发安全的,当你尝试多个 Goroutine 操作同一个 map,会产生报错:fatal error: concurrent ...

随机推荐

  1. npm i -D和-s及-g以及--save的那些事

      i 是 install 的简写 -S 就是 --save 的简写 -D 就是 --save-dev 的简写 npm i module_name -S = > npm install modu ...

  2. Linux通过fdisk或者parted进行磁盘分区,然后格式化和挂载磁盘

    磁盘分区是Linux的常用命令,其中fdisk和parted命令最为常用.但是当磁盘大于2T时,fdisk只能分到2T. 比如4T的磁盘,fdisk只能分2T的主分区出来,parted可以直接分成4T ...

  3. 顺着这份Java面试地图,国内一二线互联网公司随便进...

    原创:陶朱公Boy(微信公众号ID:taozhugongboy),欢迎分享,转载请保留出处. 前言 临近春节,这几天手头没什么事情,花了点时间,将自己近两年收集的面试真题,进行了一番深度归纳总结,整理 ...

  4. 从零开始的react入门教程(四),了解常用的条件渲染、列表渲染与独一无二的key

    壹 ❀ 引 在从零开始的react入门教程(三),了解react事件与使用注意项一文中,我们了解了react中事件命名规则,绑定事件时对于this的处理,以及事件中可使用的e对象.那么这篇文章中我们来 ...

  5. JS leetcode 存在重复元素 II 题解分析,记一次震惊的负向优化

    壹 ❀ 引 整理下今天做的算法题,题目难度不高,但在优化角度也是费了一些功夫.题目来自219. 存在重复元素 II,问题描述如下: 给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i ...

  6. 硬件开发笔记(二):硬件开发基本流程,制作一个USB转RS232的模块(一):开发基本过程和元器件选型

    前言   做个usb转串口,同时兼容ttl,讲述硬件模块基础的开发流程,本篇描述了全流程过程,然后选型了合适的元器件.   基本流程   以下是笔者个人从事过相关硬件研发,总结出来的流程,仅代表个人意 ...

  7. 【LeetCode滑动窗口专题】水果成篮 + 最小覆盖子串(hard)+ 字符串的排列

    二刷刷到滑动窗口,发现有一些细节和遗漏,在此补充 实际上关于滑动窗口的题还有一题:最小长度的子数组 进入正题 水果成篮 LeetCode904水果成篮 你正在探访一家农场,农场从左到右种植了一排果树. ...

  8. 4-request对象

    前端提交数据 必备知识点 前端form表单中action属性,不写默认是当前路由地址 前端form表单中的method属性,不写默认是GET请求 前端页面 templates\register.htm ...

  9. NodeJS开启GZIP功能

    gzip是用于压缩,效果特别好,js.css等文件的压缩率一般高达70% 具体方法如下: 先安装一个依赖 npm install compression --save 然后在已有代码里(一般是app. ...

  10. .NET应用国际化支持-葡萄牙语下如何不区分重音的模糊查询

    葡萄牙语,作为一种罗曼语族的语言,其正字法(orthography)并不使用音标系统来标记发音,而是有一套特定的拼写规则.然而,葡萄牙语中确实使用重音符号(acentos)来标记某些元音的重音(str ...