go sync.Map源码分析

news2024/9/21 18:49:37

概述

go 语言中的map并不是并发安全的,在Go 1.6之前,并发读写map会导致读取到脏数据,在1.6之后则程序直接panic. 因此之前的解决方案一般都是通过引入RWMutex(读写锁)进行处理, 关于go为什么不支持map的原子操作,概况来说,对map原子操作一定程度上降低了只有并发读,或不存在并发读写等场景的性能. 但作为服务端来说,使用go编写服务,大部分情况下都会存在gorutine并发访问map的情况,因此,1.9之后,go 在sync包下引入了并发安全的map. 这里将从源码对其进行解读.

1. sync.Map提供的方法

  • 存储数据,存入key以及value可以为任意类型.
func (m *Map) Store(key, value interface{}) 
  • 删除对应key
func (m *Map) Delete(key interface{}) 
  • 读取对应key的值,ok表示是否在map中查询到key
func (m *Map) Load(key interface{}) (value interface{}, ok bool) 
  • 针对某个key的存在读取不存在就存储,loaded为true表示存在值,false表示不存在值.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 
  • 表示对所有key进行遍历,并将遍历出的key,value传入回调函数进行函数调用,回调函数返回false时遍历结束,否则遍历完所有key.
func (m *Map) Range(f func(key, value interface{}) bool) 

2. 原理

通过引入两个map,将读写分离到不同的map,其中read map只提供读,而dirty map则负责写. 这样read map就可以在不加锁的情况下进行并发读取,当read map中没有读取到值时,再加锁进行后续读取,并累加未命中数,当未命中数到达一定数量后,将dirty map上升为read map.

另外,虽然引入了两个map,但是底层数据存储的是指针,指向的是同一份值.

具体流程: 如插入key 1,2,3时均插入了dirty map中,此时read map没有key值,读取时从dirty map中读取,并记录miss数

当miss数大于等于dirty map的长度时,将dirty map直接升级为read map,这里直接 对dirty map进行地址拷贝.

当有新的key 4插入时,将read map中的key值拷贝到dirty map中,这样dirty map就含有所有的值,下次升级为read map时直接进行地址拷贝.

3. 源码分析

3.1 主要结构

entry结构,用于保存value的interface指针,通过atomic进行原子操作.

type entry struct {
	p unsafe.Pointer // *interface{}
} 

Map结构, 主结构,提供对外的方法,以及数据存储.

type Map struct {
	mu Mutex//存储readOnly,不加锁的情况下,对其进行并发读取
	read atomic.Value // readOnly//dirty map用于存储写入的数据,能直接升级成read map.
	dirty map[interface{}]*entry//misses 主要记录read读取不到数据加锁读取read map以及dirty map的次数.
	misses int
} 

readOnly 结构, 主要用于存储

// readOnly 通过原子操作存储在Map.read中, 
type readOnly struct {
	m map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
} 

3.1 Load方法

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		//加锁,然后再读取一遍read map中内容,主要防止在加锁的过程中,dirty map转换成read map,从而导致读取不到数据.read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			//记录miss数, 在dirty map提升为read map之前,//这个key值都必须在加锁的情况下在dirty map中读取到.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
} 

3.2 Store方法

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {//如果在read map读取到值,则尝试使用原子操作直接对值进行更新,更新成功则返回
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}//如果未在read map中读取到值或读取到值进行更新时更新失败,则加锁进行后续处理
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {//在检查一遍read,如果读取到的值处于删除状态,将值写入dirty map中
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}//使用原子操作更新key对应的值
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {//如果在dirty map中读取到值,则直接使用原子操作更新值
		e.storeLocked(&value)
	} else {//如果dirty map中不含有值,则说明dirty map已经升级为read map,或者第一次进入//需要初始化dirty map,并将read map的key添加到新创建的dirty map中.
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
} 

3.3 LoadOrStore方法

代码逻辑和Store类似

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	// 不加锁的情况下读取read map
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {//如果读取到值则尝试对值进行更新或读取
		actual, loaded, ok := e.tryLoadOrStore(value)
		if ok {
			return actual, loaded
		}
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)// 在加锁的请求下在确定一次read map
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		actual, loaded, _ = e.tryLoadOrStore(value)
	} else if e, ok := m.dirty[key]; ok {
		actual, loaded, _ = e.tryLoadOrStore(value)
		m.missLocked()
	} else {
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
		actual, loaded = value, false
	}
	m.mu.Unlock()

	return actual, loaded
} 

3.4 Range 方法

func (m *Map) Range(f func(key, value interface{}) bool) {//先获取read map中值
	read, _ := m.read.Load().(readOnly)//如果dirty map中还有值,则进行加锁检测
	if read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)		if read.amended {//将dirty map中赋给read,因为dirty map包含了所有的值
			read = readOnly{m: m.dirty}
			m.read.Store(read)
			m.dirty = nil
			m.misses = 0
		}
		m.mu.Unlock()
	}//进行遍历
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
} 

3.5 Delete 方法

func (m *Map) Delete(key interface{}) {//首先获取read map
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()//加锁二次检测
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]//没有在read map中获取到值,到dirty map中删除
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete()
	}
} 

4. 局限性

从以上的源码可知,sync.map并不适合同时存在大量读写的场景,大量的写会导致read map读取不到数据从而加锁进行进一步读取,同时dirty map不断升级为read map. 从而导致整体性能较低,特别是针对cache场景.针对append-only以及大量读,少量写场景使用sync.map则相对比较合适.

对于map,还有一种基于hash的实现思路,具体就是对map加读写锁,但是分配n个map,根据对key做hash运算确定是分配到哪个map中. 这样锁的消耗就降到了1/n(理论值).具体实现可见:concurrent-map

相比之下, 基于hash的方式更容易理解,整体性能较稳定. sync.map在某些场景性能可能差一些,但某些场景却能取得更好的效果. 所以还是要根据具体的业务场景进行取舍.

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

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

相关文章

多线程看这一篇文章就够了

第一章 多线程概述 1 2 31. 什么是程序? 2. 什么是进程? 3. 什么是线程?程序1是为完成特定任务、用某种语言编写的一组指令的集合(一段静态的代码)进程1是程序的一次执行过程,或是正在运行的一个程序线程1 2 3进程可进一步细化为线程,是一个程序内部的…

软考网络工程师上午常考点

软考网络工程师上午常考点: **计算机硬件基础:**根据考纲分析,本章主要考查三个模块:计算机体系结构、存储系统、I/O输入输出系统,其中每一模块又分若干知识点。“计算机硬件基础”相当于软考中的“公共基础课”&…

12月更新 | Visual Studio Code Python

我们很高兴地宣布,2022年12月发布的适用于 Visual Studio Code Python 和 Jupyter 扩展现已推出!此版本包括以下改进:Pylance 自动缩进 预览:浏览器中运行与调试 Python社区提供新扩展:Ruff如果您有兴趣,可…

C51单片机基础之串口编程实战

目录 一、串口编程寄存器分析 1、PCON : 电源控制寄存器 2、SCON:串行控制寄存器 二、自己实现串口初始化编程 三、发送一串字符串给到PC端编程 四、PC发送指令控制LED编程 五、串口中断实时控制LED编程 1、串口中断的中断号:interrupt4 2、串口…

Apache Doris Join 实现与调优实践|未来源码

推荐语: SQL 的支持力度和粒度,已经作为今天所有大数据计算引擎的重要衡量标准之一,而 SQL 的所有操作,可以分为简单操作(如 where、limit等 filter 操作)和复杂操作(groupby、join 等聚合操作&…

SpringCloud微服务项目实战 - 3.App端文章

经历了新冠疫情始末,之后,多出门走走,看看山,看看海,吃好吃的 系列文章目录 项目搭建App登录及网关App文章 文章目录系列文章目录一、文章列表1. 数据库⑴. 导入文章数据库⑵. 表结构分析①. ap_article 文章基本信息…

MySQL慢SQL探究

文章目录前言1、慢SQL捕获慢查询追踪配置方式2、情况分析为什么查询会慢?2.1 SQL执行计划分析explain执行计划分析PROFILE分析OPTIMIZER_TRACE分析3、引擎参数配置分析I/O性能分析MySQL I/O参数其他原因分析网络抖动单表数据量过大总结前言 我们在日常开发中&#…

GeoServer搭建私有地图服务,Cesium引擎加载。

一、安装JDK 1、安装GeoServer是基于Java的环境,所以需要先装Jdk环境。 2、前往官网下载Java SE 3、下载GeoServer 1、前往官网下载最新稳定版 2、安装GeoServer 二、发布地图服务 1、启动geoserver 找到安装目录,bin/下的startup.bat,双击执行…

PF-Net源码详解

代码及数据见最后 1.参数配置 参数配置使用默认参数即可,但是windows操作系统下,需要将--workers设置为0. 2.数据准备 PF-Net将不完整的点云做输入,并预测其缺失部分。同时,我们可以从整体流程看到,输入有三个尺度,通过最远点采样进行构建,预测的输出也有三个尺度。数…

MySQL:SQL通用语法、SQL分类、DDL、DML、DQL、DCL

一.SQL通用语法 SQL语句可以单行或多行书写,以分号结尾;SQL语句可以使用空格/缩进来增强语句的可读性;MySQL数据库SQL语句不区分大小写,关键字建议大写;注释: 单行:--或#多行:/* …

Spring AOP 面向切面编程

1.AOP是什么我们之前学过 OOP 面向对象编程, 那么 AOP 是否就比 OOP 更牛逼呢? 是否也是面向过程到面向对象这种质的跨越呢? 其实不是, 它和 OOP 思想是一种互补的关系, 是对 OOP 思想的一种补充.AOP (Aspect Oriented Programming) : 面向切面编程, 它是一种思想, 它是对某一…

Arrays数组

1.Arrays.toString()方法:输出数组内容 2.Arrays.sort()方法:给数组排序,默认升序 对其他对象数组进行排序 一个对象数组,排序算法需要重复比较数组中的元素。不同的类比较元素的规则是不同的,但是排序算法只应该调用类提供的比较方法&#…

netty中channelHandler实现原理及最佳实践|极客星球

为持续夯实MobTech袤博科技的数智技术创新能力和技术布道能力,本期极客星球邀请了企业服务研发部工程师梁立从 TCP 的粘包/半包、 Netty 处理粘包/半包及源码分析、 开源项目对 channelHandler最佳实践三方面对《netty 中channelHandler的原理与最佳实践》进行了全面…

【Ctfer训练计划】——(九)

作者名:Demo不是emo 主页面链接:主页传送门创作初心:舞台再大,你不上台,永远是观众,没人会关心你努不努力,摔的痛不痛,他们只会看你最后站在什么位置,然后羡慕或鄙夷座右…

Python+Selenium4元素定位_web自动化(3)

目录 0. 上节回顾 1. 八大定位 2. 定位器 3. CSS选择器 4. XPath选择器 4.1. XPath语法 4.2. XPath 函数 5. 相对定位 5.1 XPath 中的相对定位【重点】 5.1.1 相对路径 5.1.2 轴 5.2 selenium4 中的相对定位 总结 0. 上节回顾 浏览器的一般操作 浏览器的高级操作…

【sciter】:JSX 组件实现数据持久化

# 原理 组件数据持久化指的是:重新加载组件后,能否将重新加载前组件所存在的数据,在重新加载后数据依旧保存在组件中。 组件数据持久化实现原理:将每次更新组件数据同步到 Storage 中。并且监听组件重新加载(刷新),在刷新前将 Storage 关闭(确保数据不丢失)。当加载…

idea中添加git使用时文件不同颜色,标签不同颜色,代码不同颜色代表的含义

文章目录文件的颜色标签的颜色合并代码时不同颜色区块的含义文件的颜色 绿色——已经加入控制暂未提交; 红色——未加入版本控制;自己建立新文件后就是红色的,出现红色的一定要Add到git中,不然不能上传到远程仓库 蓝色——加入&am…

关于markdown相关语法的学习

众所周知,一个好的项目需要搭配一个好的项目说明,就行吃饺子需要蘸醋一样,没有醋的饺子,你仅仅吃掉了他的肉体,而得不到他的灵魂。下面开始吃饺子,不对,是开始学习markdown文件的基础语法&#…

在采购管理过程中使用技术有什么好处?

采购过程不总是简单直接的,人工采购过程非常耗费人力和时间,并且涉及大量文书工作。另一方面,当你在采购过程中使用技术时,比如使用SRM采购管理系统,会节省很多时间,使整个过程变得更加简单和轻松。 在讨…

Homekit智能家居创意DIY之智能吸顶灯

买灯要看什么因素 好灯具的灯光可以说是家居的“魔术师”,除了实用的照明功能外,对细节的把控也非常到位。那么该如何选到一款各方面合适的灯呢? 照度 可以简单理解为清晰度,复杂点套公式来说照度光通量(亮度&#…