Go map 读写性能优化 - 分片 map

基本在所有的编程语言中,都有 map 这种数据结构,Go 语言也不例外。
我们知道 Go 是一门对并发支持得比较好的语言,但是 map 并不支持并发读写。
比如,下面这种写法是错误的:

var m = make(map[int]int)
var wg sync.WaitGroup
wg.Add(2)
// 启动两个协程序同时写入 map
go func() {for i := 0; i < 100; i++ {m[i] = i}wg.Done()
}()
go func() {for i := 0; i < 100; i++ {m[i] = i}wg.Done()
}()
wg.Wait()

这样写会报错:

fatal error: concurrent map writes

为什么 Go map 不支持并发读写

这跟 map 的实现有关,根本原因是:map 的底层是支持自动扩容的,在添加元素的时候,如果发现容量不够,就会自动扩容。
如果允许扩容和访问操作同时发生,那么访问到的数据就不一定就是我们之前存放进去的了,所以 Go 从设计上就禁止了这种操作。
也就是 fail fast 的原则。

至于具体为什么,我们可以看看 map 在扩容时做了什么操作:

在这里插入图片描述

Go 中 map 的扩容是一个渐进的过程,在我们访问 map 的时候,会对 map 底层实际存储数据的桶进行迁移。

如果支持并发读写,就有可能会导致底层定位到的桶是扩容前的,但是实际上数据已经迁移到了新的桶中,这样就会导致访问到的并不是我们想要的数据。

Go map 设计上的考虑

在 Go 官网的博客上有专门针对 Go 不支持并发读写的说明,大概意思是:

经过长时间讨论,Go 团队认为,多数情况下,我们并不需要从多个 goroutine 来对 map 进行安全访问,
map 可能是已经实现了同步的某些较大数据结构或计算的一部分。因此,如果底层再去实现 map 的互斥操作,
就会减慢大多数程序的速度,而只能增加少数程序的安全性。

也就是说,他们认为大多数情况下,map 通常是我们自定义数据结构的一部分,而对这个自定义数据结构的访问时,我们一般已经有了锁去保证并发读写安全了,所以没有必要再在底层的 map 上加锁,从而可以保证大多数程序的速度。

但是从语言层面上来说,我们依然可以自行通过互斥锁来实现 map 的的互斥访问。
仅当对 map 在进行更新的时候,map 的读才是不安全的,但是 map 是支持并发读的。

如何解决这个问题 - 互斥锁

关于这一点,同样可以在 Go 官方博客中找到相关的说明,在 Go map 并发这一节也给了对应的 demo。具体来说就是将一般锁跟 map 关联起来,要读写 map 的时候,得先获取这个锁才能访问,这样就避免了对 map 的并发读写了。这是最典型的一种解决方案,也是最简单的。

下面的结构体定义了一个匿名结构体 counter,这个结构体中包含了一个 sync.RWMutex 互斥锁和一个 map

var counter = struct{sync.RWMutexm map[string]int
}{m: make(map[string]int)}

读的时候,我们可以使用 RLock 获取读锁,然后访问 m 这个 map

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

RLock 是读锁,多个 goroutine 可以同时获取读锁,读锁释放之前,其他 goroutine 无法获取写锁。

写的时候,我们可以使用 Lock 获取写锁:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

Lock 是写锁,只有一个 goroutine 可以获取写锁,并且写锁释放之前,其他 goroutine 无法获取读锁,也无法获取写锁。

另一种解决方法 - sync.Map

除了使用互斥锁,我们也可以使用 Go 语言自带的 sync.Map 来解决这个问题:

var m sync.Map
var wg sync.WaitGroup
wg.Add(2)
go func() {for i := 0; i < 100; i++ {m.Store(i, i)}wg.Done()
}()
go func() {for i := 0; i < 100; i++ {m.Store(i, i)}wg.Done()
}()
wg.Wait()

虽然 sync.Map 可以实现并发的读写,但是底层上依然会有较多的竞态条件,所以性能上并不是最好的,本质上还是操作一个 map
只是通过一些原子操作 + 自旋锁来实现并发安全的读写。

而且 sync.Map 设计出来的时候是为了应对一些特定的场景的,具体来说有以下两个场景:

  1. 当给定 key 的条目只写入一次但读取多次时,如在只会增长的缓存中。(读多写少)
  2. 当多个 goroutine 读取、写入和覆盖不相交的键集的条目。(不同 goroutine 操作不同的 key)

在这两种情况下,可以获得比用 Mutex + mapRWMutex + map 更好的性能,因为很多的锁操作都变成了原子操作。

具体细节可参考我此前的一篇文章:《深入理解 go sync.Map - 基本原理》

互斥锁、sync.Map 还不是最优的解决方案

使用互斥锁或者 sync.Map 的方式,虽然都可以解决 map 并发读写的问题,但是性能上都不是最优的。

因为它们底层还是会有互斥锁的竞争。这就意味着,在进行写 map 操作时,可能会存在较多的锁竞争,从而导致性能下降。

map 分片

如果我们有了解过 MongoDB,就会知道,MongoDB 中也有分片的概念,当数据量过大时,
单个 MongoDB 实例可能无法存储所有的数据,或者单个实例无法处理过多的读写请求,
这时候就需要将数据分片存储到多个 MongoDB 实例中,也就是按照一定的规则将数据存储到不同的机器上,
然后读写数据的请求也会依据一定规则被路由到对应的机器上。

同样的,如果我们的 map 并发请求过多,那么我们也可以将 map 分片,
将不同的 key 存储到不同的 map 中,这样就可以避免 map 的并发读写了。

我们需要做的是:通过 key 来计算其 hash 值,然后根据 hash 值来决定将 key 存储到哪个 map 中,
同时,我们每一个 map 都需要加上互斥锁,这样就可以保证每一个 map 的并发安全了。

具体如下图:

在这里插入图片描述

说明:

  1. 图中的 G0~2 表示 goroutinelock0~2 表示不同的互斥锁,map shard 0~2 表示多个 map 分片。
  2. goroutine 中会根据 key 计算出 hash 值,然后根据 hash 值来决定将 key 存储到哪个 map 分片中,然后获取这个分片对应的锁,然后进行读写操作。

虽然上图画起来是 G1 不会访问到 shard 0 或者 shard 2,但实际上是有可能的,上图只是想说明:
多个 goroutine 可以多个锁来访问多个 map 分片,而不用像之前那样多个 goroutine 都来竞争同一把锁了。
也就减少了锁的竞争和等待

代码实现

具体实现已经有一个开源的库了:orcaman/concurrent-map,
可以在 github 上搜到。

下面是它的部分代码:

var SHARD_COUNT = 32// A "thread" safe map of type string:Anything.
// To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards.
type ConcurrentMap[K comparable, V any] struct {shards   []*ConcurrentMapShared[K, V]sharding func(key K) uint32
}// A "thread" safe string to anything map.
type ConcurrentMapShared[K comparable, V any] struct {items        map[K]Vsync.RWMutex // Read Write mutex, guards access to internal map.
}// GetShard returns shard under given key
func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] {return m.shards[uint(m.sharding(key))%uint(SHARD_COUNT)]
}

说明:

  1. SHARD_COUNT 是分片数量,也就是底层会有多少个 map 分片。
  2. ConcurrentMap 表示一个支持并发读写的 map,它底层包含了多个 map 分片,以及有一个根据 key 计算分片的函数。
  3. ConcurrentMapShared 表示一个 map 分片,也就是上面提到的 map + RWMutex 组合。
  4. GetShard 根据 key 获取对应的 map 分片。

单从代码的角度,它封装了更多的东西,性能相比单纯的 map + RWMutex 自然会差一点,
但是从并发读写的角度来说,它比单纯 map + RWutex 要好很多。
因为它将原本只支持一个协程写的 map 转换为了支持多个协程写操作的 map,一定程度上提高了并发

但是需要注意的是,我们需要频繁写同一个 key 的操作,上面这种分片的方式也不能带来性能上的提升。
分片的方式更适合那些 key 区分度高的、写操作频繁的场景。

总结

最后再简单回顾一下本文所讲内容:

  1. Go 的 map 设计上不支持并发读写,如果我们有并发读写操作会直接 panic
  2. Go 的设计者们认为,多数情况下,我们并不需要从多个 goroutine 来对 map 进行安全访问,所以他们没有在底层实现 map 的互斥操作。
  3. 有两种方法可以解决 map 并发读写的问题:互斥锁、sync.Map。但是 sync.Map 设计上是应对某些特定场景的,并不合适所有场景。
  4. 我们可以通过分片的方式来解决 map 并发读写的问题,这样可以减少锁的竞争,提高并发读写性能。目前已经有现成的开源库可以使用了。

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

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

相关文章

如何下载52pojie、CSDN、简书、Myitmx、博客园的文章?(最新教程)

使用的油猴插件&#xff0c;具体怎么安装问一下度娘。 我用的火狐&#xff0c;点点点就行了&#xff0c;省事 先安装油猴拓展&#xff0c;启用一下 Tampermonkey – 下载 &#x1f98a; Firefox 扩展&#xff08;zh-CN&#xff09; 在安装插件 SaveToPDF 脚本安装后&#…

基于EdgeWorkers的边缘应用如何进行单元测试?

随着各行各业数字化转型的持续深入&#xff0c;越来越多企业开始选择将一些应用程序放在距离最终用户更近的边缘位置来运行&#xff0c;借此降低延迟&#xff0c;提高应用程序响应速度&#xff0c;打造更出色的用户体验。 相比传统集中部署和运行的方式&#xff0c;这种边缘应…

基于 NOVATEK NT98530 Multiview Stitching 应用解决方案

感测技术近来于影像监控系统应用有了进一步的发展&#xff0c;多镜头的应用也与日俱增&#xff0c;如 AI 视觉感测会议相机&#xff0c;能满足远端多人聚会、远距教育训练的多元需求等&#xff0c;相关应用层面广泛涵盖了在生活中所面对的各种场景&#xff0c;带动更加可观的潜…

#{}和${}的区别

#{}和${}的区别 .本质区别:使用注意事项防止SQL注入排序like查询 . 在使用mybatis操作数据库的时候,我们在编写sql语言的时候,会遇到一个问题,就是在传参的时候,有两个符号#,$.这两个符号有什么异同呢,接下来,我就会带着大家对这个问题进行简单的探讨 本质区别: #执行的是预编…

idea自动生成实体类

第一步&#xff1a;idea连接数据库 出现这个就连接成功 第二步&#xff1a;选择数据库 第三步&#xff1a;创建实体类 也可以点击数据库一下子全部创建 选择创建实体类所放位置 这样就完成了&#xff0c;点击看看对其做相应修改

RK3588平台开发系列讲解(视频篇)RKMedia的VDEC模块

文章目录 一、 VDEC模块支持的编码标准介绍二、VDEC API的调用三、VDEC解码流程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢RKMedia是RK提供的一种多媒体处理方案,可实现音视频捕获、音视频输出、音视频编解码等功能。 一、 VDEC模块支持的编码标准介绍 RK3688 V…

【日常总结】MobaXterm session 如何迁移

一、场景 二、解决方案 三、实战 Stage 1&#xff1a;右键导出 Import sessions from file Stage 2&#xff1a;新MobaXterm软件中导入即可。Export all sessions to file 四、不足 一、场景 电脑更换&#xff0c;原电脑上MobaXterm中的20多个连接如何迁移 二、解决方案 …

如何判断 LM358 芯片是否损坏或故障?

LM358 芯片是一种流行的低功耗双运放&#xff0c;广泛应用于各种电子电路中&#xff0c;包括放大器、滤波器、积分器、比较器等。它以其低成本、高性价比和广泛的工作电源范围&#xff08;3V至32V单电源或1.5V至16V双电源&#xff09;而被广泛使用。 然而&#xff0c;像所有电…

十分钟学会用springboot制作微信小程序富文本编辑器

1.1 富文本模型设计 在构建富文本编辑器系统时&#xff0c;首先需要设计一个合适的富文本模型。 CREATE TABLE IF NOT EXISTS rich_texts (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(255),content TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这个表包括…

如何在FBX剔除Lit.shader依赖

1&#xff09;如何在FBX剔除Lit.shader依赖 2&#xff09;Unity出AAB包&#xff08;PlayAssetDelivery&#xff09;模式下加载资源过慢问题 3&#xff09;如何在URP中正确打出Shader变体 4&#xff09;XLua打包Lua文件粒度问题 这是第371篇UWA技术知识分享的推送&#xff0c;精…

维护神经健康:新生儿补充维生素B12的注意事项

引言&#xff1a; 维生素B12是维持神经系统正常运作所必需的关键成分&#xff0c;对于新生儿的生长发育具有重要的作用。本文将深入探讨维生素B12的功能、新生儿补充的必要性&#xff0c;以及在补充维生素B12时应该注意的事项&#xff0c;以为父母提供科学、全面的育儿指南。 …

springCloud gateway 防止XSS漏洞

springCloud gateway 防止XSS漏洞 一.XSS(跨站脚本)漏洞详解1.XSS的原理和分类2.XSS漏洞的危害3.XSS的防御 二.Java开发中防范XSS跨站脚本攻击的思路三.相关代码&#xff08;适用于spring cloud gateway&#xff09;1.CacheBodyGlobalFilter.java2.XssRequestGlobalFilter.java…

C++基础语法学习笔记

C Tutorial 1.基础语法 C 应用&#xff1a;操作系统、图形用户界面和嵌入式系统 C和C区别&#xff1a;C支持类和对象 C语法 #include <iostream> using namespace std;int main(){cout << "hello world!";return 0; }int main () { cout << &q…

8-Docker网络模式之container

1.介绍 Docker container网络模式是指指定新创建的Docker容器和已经存在的一个容器共享一个Network Namespace,而不是和宿主机共享。 使用Docker container网络模式新建的容器不会创建的自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口等。然而两个容器除了网络…

知识图谱推理方法综述

目录 前言1 基于符号表示的推理方法1.1 Axioms和Datalog1.2 图结构和规则学习1.3 TBox和ABox 2 基于向量表示的推理方法2.1 嵌入系列和Ontology Embedding2.2 向量的规则学习2.3 图神经网络 3 符号逻辑和表示学习的融合3.1 向量表示学习与本体规则学习3.2 规则演绎推理3.3 符号…

Oracle闪回日志管理(flashbackup log)

Oracle闪回日志管理&#xff08;flashbackup log&#xff09; 1.开启闪回日志 shutdown immediate startup mount; alter database archivelog; alter database flashback on; alter database open; 2、查看闪回日志保留期限 show parameter flash 默认是1440分钟&#xff0c…

软件工程知识梳理4-详细设计

详细设计阶段的根本目标是确定应该怎样具体地实现所要求的系统&#xff0c;也就是说.经过这个阶段的设计工作.应该得出对目标系统的精确描述.从而在编码阶段可以把这个描述直接翻译成用某种程序设计语言书写的程序。 详细设计的的目标不仅仅是逻辑上正确地实现每个模块地功能&a…

手机屏幕生产厂污废水处理需要哪些工艺设备

随着手机行业的快速发展&#xff0c;手机屏幕生产厂的规模也越来越大&#xff0c;但同时也带来了大量的污废水排放问题。为了保护环境和人类的健康&#xff0c;手机屏幕生产厂需要采取适当的工艺设备来处理污废水。本文将介绍手机屏幕生产厂污废水处理所需的工艺设备。 首先&am…

计算机速成课Crash Course - 27. 3D 图形

今天继续计算机速成课Crash Course的系列讲解。 更多技术文章&#xff0c;全网首发公众号 “摸鱼IT” 锁定 -上午11点 - &#xff0c;感谢大家关注、转发、点赞&#xff01; 计算机速成课Crash Course - 27. 3D 图形 (qq.com) 27. 3D 图形 在过去五集&#xff0c;我们从基于…

Wireshark网络协议分析 - Wireshark速览

在我的博客阅读本文 文章目录 1. 版本与平台2. 快速上手2.1. 选择网络接口进行捕获&#xff08;Capture&#xff09;2.2. 以Ping命令为例进行抓包分析2.3. 设置合适的过滤表达式2.4. 数据包详情2.5. TCP/IP 四层模型 3. 参考资料 1. 版本与平台 Wireshark是一个开源的网络数据…