golang并发安全-sync.map

sync.map解决的问题

golang 原生map是存在并发读写的问题,在并发读写时候会抛出异常

func main() {mT := make(map[int]int)g1 := []int{1, 2, 3, 4, 5, 6}g2 := []int{4, 5, 6, 7, 8, 9}go func() {for i := range g1 {mT[i] = i}}()go func() {for i := range g2 {mT[i] = i}}()time.Sleep(3 * time.Second)
}

抛出异常 fatal error: concurrent map writes

如果将map换成sync.map 那么就不会出现这个问题,下面就简单说说syn.map怎么实现的

基本结构

Map结构体

// Map类型针对两种常见的用例进行了优化:1-当给定键的条目只写一次但读多次时,如在只增长的缓存中,2-当多个goroutine读取、写入和覆盖不相交的键集的条目时。在这两种情况下,与单独的Mutex或RWMutex配对的Go映射相比,使用Map可以显著减少锁争用。
type Map struct { // 互斥锁mu,操作dirty需先获取mu mu Mutex // read是只读的数据结构,可安全并发访问部分,访问它无须加锁,sync.map的所有操作都优先读read // read中存储结构体readOnly,readOnly中存着真实数据,储存数据时候需要加锁// read中可能会存在脏数据:即entry被标记为已删除read atomic.Value // readOnly// dirty是可以同时读写的数据结构,访问它要加锁,新添加的key都会先放到dirty中 // dirty == nil的情况:// 1.被初始化 // 2.提升为read后,但它不能一直为nil,否则read和dirty会数据不一致。 // 当有新key来时,会用read中的数据(不是read中的全部数据,而是未被标记为已删除的数据,)填充dirty // dirty != nil时它存着sync.map的全部数据(包括read中未被标记为已删除的数据和新来的数据)dirty map[interface{}]*entry // 统计访问read没有未命中然后穿透访问dirty的次数 // 若miss等于dirty的长度,dirty会提升成read,提升后可以增加read的命中率,减少加锁访问dirty的次数    misses int
}

 readOnly结构体

//第一点的结构read存的就是readOnly
type readOnly struct {m       map[any]*entry //m是一个map,key是interface,value是指针entry,其指向真实数据的地址,amended bool  // amended等于true代表dirty中有readOnly.m中不存在的entry。
}

entry结构体

type entry struct { // p://     expunged: 删除; nil: 逻辑删除但存在dirty; 数据  p unsafe.Pointer // *interface{}
}

Load方法

代码解说

Load:读取数据

// Load 返回 map 中key 对应的值,如果没有值,则返回 nil。
// ok 结果表示是否在 map 中找到了 value。
func (m *Map) Load(key any) (value any, ok bool) {read, _ := m.read.Load().(readOnly) // 从read 读取数据,并转换readonlye, ok := read.m[key]if !ok && read.amended { // readonly没有找到对应数据m.mu.Lock()// 双重检测:// 再检查一次readonly,以防中间有Map.dirty被替换为readonlyread, _ = m.read.Load().(readOnly)e, ok = read.m[key]if !ok && read.amended { // 去 dirty查找对应数据e, ok = m.dirty[key]// 无论Map.dirty中是否有这个key,miss都加一,// 若miss大小等于dirty的长度,dirty中的元素会被加到Map.read中 m.missLocked()}m.mu.Unlock()}if !ok {return nil, false}return e.load()// 若entry.p被删除(等于nil或expunged)返回nil和不存在(false),否则返回对应的值和存在(true)    
}

missLocked:dirty是如何提升为read

func (m *Map) missLocked() {m.misses++ // 每次misses+1if m.misses < len(m.dirty) {return}// 当misses等于dirty的长度,m.dirty转换readOnly,amended被默认赋值成false  m.read.Store(readOnly{m: m.dirty})m.dirty = nilm.misses = 0
}

流程图

 load: 会先从readOnly查找数据, 如果没有开启加锁,再次访问readOnly, 再次没有再去dirty去查。

Store方法

代码解说

store: 赋值

// Store 设置key value
func (m *Map) Store(key, value any) {read, _ := m.read.Load().(readOnly) // 转换readOnly// 若key在readOnly.m中且 e.tryStore 不为 false(没有逻辑删除)if e, ok := read.m[key]; ok && e.tryStore(&value) {return}m.mu.Lock()// 双重检测:// 再检查一次readonly,以防中间有Map.dirty被替换为readonlyread, _ = m.read.Load().(readOnly)if e, ok := read.m[key]; ok {// entry.p状态是expunged置为nil// 如果是逻辑删除就需要清除标记了if e.unexpungeLocked() {// 之前dirty中没有此key,所以往dirty中添加此key              m.dirty[key] = e}// cas: 赋值e.storeLocked(&value)} else if e, ok := m.dirty[key]; ok {e.storeLocked(&value)} else {// dirty中没有新数据,往dirty中添加第一个新key        if !read.amended {// 把readOnly中未标记为删除的数据拷贝到dirty中            m.dirtyLocked()// amended:true,现在dirty有readOnly中没有的key            m.read.Store(readOnly{m: read.m, amended: true})}m.dirty[key] = newEntry(value)}m.mu.Unlock()
}

tryStore:尝试写入数据

func (e *entry) tryStore(i *any) bool {for {   p := atomic.LoadPointer(&e.p)    if p == expunged {   // 如果逻辑删除就返回false    return false   }    // 不是就将value写入if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {      return true    }  }
}

dirtyLocked: 将readOnly 未删除的放到dirty

func (m *Map) dirtyLocked() { if m.dirty != nil {  return }       // dirty为nil时,把readOnly中没被标记成删除的entry添加到dirty read, _ := m.read.Load().(readOnly)  m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m {               // tryExpungeLocked函数在entry未被删除时返回false,反之返回true    if !e.tryExpungeLocked() {    // entry没被删除    m.dirty[k] = e }  }
}

流程图

sync.map不适合用于频繁插入新key-value的场景,因为此操作会频繁加锁访问dirty会导致性能下降。更新操作在key存在于readOnly中且值没有被标记为删除(expunged)的场景下会用无锁操作CAS进行性能优化,否则也会加锁访问dirty。

Delete方法

代码解说

LoadAndDelete:查找删除

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {read, _ := m.read.Load().(readOnly) e, ok := read.m[key]if !ok && read.amended { // readOnly不存在此key,但dirty中可能存在               // 加锁访问dirty   m.mu.Lock()                // 双重检测 read, _ = m.read.Load().(readOnly)    e, ok = read.m[key]                // readOnly不存在此key,但是dirty中可能存在    if !ok && read.amended {      e, ok = m.dirty[key]      delete(m.dirty, key)      m.missLocked()   // 判断dirty是否可以转换readOnly,可以就转换}   m.mu.Unlock()  }  if ok {                // 如果entry.p不为nil或者expunged,则把逻辑删除(标记为nil)    return e.delete()  } return nil, false
}

delete:逻辑删除

func (e *entry) delete() (value any, ok bool) {for {p := atomic.LoadPointer(&e.p)if p == nil || p == expunged { // 已经处理或者不存在return nil, false}if atomic.CompareAndSwapPointer(&e.p, p, nil) { // 逻辑删除return *(*any)(p), true}}
}

流程图

Range方法

代码解说

Range:轮训元素

func (m *Map) Range(f func(key, value any) bool) {read, _ := m.read.Load().(readOnly)     if read.amended { // 如果dirty存在数据m.mu.Lock()         // 双重检测      read, _ = m.read.Load().(readOnly)         if read.amended {              // readOnly.amended被默认赋值成false             read = readOnly{m: m.dirty}              m.read.Store(read)              m.dirty = nil              m.misses = 0        }        m.mu.Unlock()    }     // 遍历readOnly.m   for k, e := range read.m {          v, ok := e.load()         if !ok {             continue          }          if !f(k, v) { break         }     }
}

流程图 

Range:全部key都存在于readOnly中时,是无锁遍历的,性能最优。如果readOnly只存在Map中的部分key时,会一次性加锁拷贝dirty的元素到readOnly,减少多次加锁访问dirty中的数据。

总结

1- sync.map 结构体加了readOnly 和 dirty 来实现读写分离,load,store, delete,range 每次都会优先访问read,后面访问dirty都会双重检测以防加锁前Map.dirty可能已被提升为read

2- sync.map不适合写多读少,从store 代码中可以看出会频繁加锁访问dirty,双重检测等等,这些都会导致性能下降

3- sync.map 没有提供对read, dirty 的长度方法,这个对象使用在于并发场景下,会额外带来锁竞争的问题

4- misses 是 统计访问read没有未命中然后穿透访问dirty的次数 ,如果等于dirty会转换readOnly

5- entry 有三种类型 expunged: 删除; nil: 逻辑删除但存在dirty; 数据 。其中expunged 会在 unexpungeLocked 方法中进行赋值(在store时候会加锁访问dirty,把readOnly中的未被标记为删除的所有entry指针放到dirty,之前被delete方法标记为删除状态的entry=nil都变为expunged,那这些被标记为expunged的entry将不会出现在dirty中。)

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql 将数据按照年月分组 统计

要的效果: 方案&#xff1a; ① 使用 DATE_FORMAT(date, ‘%Y-%m-%d’) 函数 DATE_FORMAT 怎么去使用格式化&#xff0c;取决于后面的格式模式。 我们这里只是想区分到年 、月&#xff0c; 所以我们的sql 里面使用 %Y-%m : SELECT DATE_FORMAT(create_time, %Y-%m) AS …

Bind for 0.0.0.0:2379 failed: port is already allocated

1、执行命令docker-compose -p docker-apisix up -d 报错 Error response from daemon: driver failed programming external connectivity on endpoint docker-apisix-etcd-1 (2a92a0cefff9194fcd1dad4bdeabf4201d9047ec2633eda455c6e46528668af4): Bind for 0.0.0.0:2379 fa…

c++输入输出流和文件操作总结

目录 一、c的输入输出流——> 指的是字节流的数据传送;具有类型安全和可扩展性。 二、流的出入路径 三、c流类库 ①概览 ②标准输出流&#xff1a; ③标准输入流&#xff1a; 四、文件操作&#xff08;ascii文件和二进制文件&#xff09; 五、字符串流&#xff08;或称…

【力扣题解】P94-二叉树的中序遍历-Java题解

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【力扣题解】 文章目录 【力扣题解】P94-二叉树的中序遍历-Java题解&#x1f30f;题目描述&#x1f4a1;题解&#x1f30f…

oracle与mysql的分析函数(窗口函数)

分析函数定义 在SQL语句中&#xff0c;很多查询语句需要进行GROUP BY分组汇总&#xff0c;但是一旦经过分组&#xff0c;SELECT返回的记录数就会减少。为了保留所有原始行记录&#xff0c;并且仍可以进行分组数据分析&#xff0c;分析函数应运而生。 Oracle 8i 版本开始支持窗…

C++实现令牌桶过滤算法

什么是令牌桶算法 令牌桶算法通过限制令牌桶的固定容量&#xff0c;实现对资源以及流量的延迟控制。请求者需先获取令牌&#xff0c;方可执行动作。若令牌桶内具有足够令牌便可通过消耗相等数量放过请求&#xff1b;而若令牌不足&#xff0c;则会拒绝请求。 该算法具备平滑的…

C语言停车场模型详解

C语言停车场模型详解 1. 引言2. 代码概述3. 代码详解3.1 定义常量和数据结构3.2 初始化车库3.3 查找车辆所在车库3.4 查找车辆所在的车位3.5 打印车库状态3.6 打印等候车辆3.7 车辆入库3.8 车辆出库3.9 菜单功能3.10 主函数 5.效果展示5.完整代码6. 总结 1. 引言 本文将介绍一…

Android : 画布绘制矩形和文字 让其居中显示简单应用

示例图&#xff1a; CenterView.java package com.example.demo;import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import android.view.View;public class Center…

生成式 AI 原生开发

如何成为生成式AI原生开发者&#xff0c;快速进入&#xff1a; 下一站 GenAI QCon 上海站喊你上车啦&#xff01; 无限构建&#xff0c;成为生成式 AI 原生开发者&#xff0c;12 月 28 日&#xff0c;下一站 GenAI 巴士即将抵达 QCon 全球软件开发大会上海站&#xff0c;码…

人大女王金融硕士为何是在职金融人士提升自己的首选?一起来看看

在这个经济飞速发展的时代&#xff0c;职场竞争愈发激烈&#xff0c;如果一味的安于现状&#xff0c;那么很有可能被社会所淘汰。近年来&#xff0c;金融行业的发展更是迅速&#xff0c;对于高端人才的需求也越来越大。所以对于从事这行的工作者来说&#xff0c;在职研究生就是…

QML —— RadioButton的两个经典示例(附完整源码)

示例1-效果 示例2-效果 实例1 - 源码 import QtQuick 2.12 import QtQuick.Window 2.12import QtQuick.Layouts 1.12 import QtQuick.Controls 2.5Window {visible: truewidth: 640height: 480title: qsTr("Hello World")Text{id: classNametext: qsTr("--&quo…

金融贷款行业怎么精准获客,电销打吐了!教你两招

助贷行业&#xff0c;一般都会设有kpi&#xff0c;压力竞争都不小。虽然现在贷款电销大家对此意见不同&#xff0c;但是&#xff0c;不可否认&#xff0c;这个行业&#xff0c;电销就是它们最快速的获客方式。只要耐得住性子&#xff0c;有一份精准的话单&#xff0c;就可以开工…

在线教育系统源码解读:定制化企业培训APP的开发策略

当下&#xff0c;企业培训正经历着一场数字化的迭代&#xff0c;定制化企业培训APP应运而生&#xff0c;成为提升员工技能、推动企业发展的重要工具。下文小编将与大家一同深入了解在线教育系统的源码&#xff0c;探讨开发定制化企业培训APP的策略&#xff0c;以满足不同企业的…

AI大模型引领未来智慧科研暨ChatGPT在地学、GIS、气象、农业、生态、环境等领域中的高级应用

以ChatGPT、LLaMA、Gemini、DALLE、Midjourney、Stable Diffusion、星火大模型、文心一言、千问为代表AI大语言模型带来了新一波人工智能浪潮&#xff0c;可以面向科研选题、思维导图、数据清洗、统计分析、高级编程、代码调试、算法学习、论文检索、写作、翻译、润色、文献辅助…

yolov8 细胞分割数据集准备及训练

1、数据 下载:https://universe.roboflow.com/motherson-hm/5-part-diff 500来张,5个类别(嗜碱性细胞、嗜酸细胞、淋巴细胞、单核细胞、中性粒细胞) yolo 分割数据标注格式: 与检测类似,就是坐标分割有多个 2、训练 训练yaml: seg_data.yaml (与检测格式一样) …

代码随想录算法训练营day1|704.二分查找、27.移除元素

第一章 数组 part01 今日任务 数组理论基础&#xff0c;704. 二分查找&#xff0c;27. 移除元素 详细布置 数组理论基础 文章链接&#xff1a;代码随想录 题目建议&#xff1a; 了解一下数组基础&#xff0c;以及数组的内存空间地址&#xff0c;数组也没那么简单。 704. 二…

玩转安卓手机录屏,轻松掌握录屏技巧!

“安卓手机有录屏功能吗&#xff1f;新买了一部安卓手机&#xff0c;因为之前都是在用苹果手机&#xff0c;所以有点不习惯&#xff0c;最近需要用到录屏功能&#xff0c;但怎么都找不到&#xff0c;希望大家教教我&#xff0c;安卓手机怎么录屏&#xff1f;” 在现代社交媒体…

Redis中RDB和AOF

Redis中RDB和AOF 定时间间隔执行数据集的时间快照&#xff0c;把某一时刻数据和妆容以文件的形式写到磁盘上&#xff0c;也就是快照。 配置文件 如果是普通安装方式可以跳过&#xff0c;如果是docker安装&#xff0c;需要到官网下载redis.conf配置文件到本地&#xff0c;地址…

常见HTTP 500错误发生原因及解决办法剖析

​  对于网站运营者来说&#xff0c;提到500内部服务器错误并不陌生。互联网行业对它的称呼有好几种&#xff0c;如“500内部服务器错误”、“HTTP 500 - 内部服务器错误”、“临时错误 (500)”、“内部服务器错误”。尽管叫法不同&#xff0c;但根本问题是相同的。 目前&…

单片机外设矩阵键盘之行列扫描识别原理与示例

单片机外设矩阵键盘之行列扫描识别原理与示例 1.概述 这篇文章介绍单片机通过行列扫描的方式识别矩阵键盘的按键&#xff0c;通过程序执行相应的操作。 2.行列扫描识别原理 2.1.独立按键识别原理 为什么需要矩阵按键 独立按键操作简单&#xff0c;当数量较多时候会占用单片机…