首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Golang map 并发读写问题源码分析

Golang map 并发读写问题源码分析

原创
作者头像
Lynalmost
发布于 2022-06-27 09:13:22
发布于 2022-06-27 09:13:22
1.6K0
举报

map介绍及问题描述

map主要用来存储kv数据,其底层使用的是开链法去冲突的hashtable,拥有自动扩容机制。使用map最方便的一点是可以O(1)快速查询(目前slice并没有提供查询接口,只能通过自己写算法实现某个元素是否存在)。

map虽然好用,但是可能不适用。

但是map有一个非常致命的坑点,在并发场景下,并发读/写都可能会出现fatal error:concurrent map read and map write的错误,刚开始使用map的时候天真的认为只要不对同一个key进行并发操作就行,但是现实很骨感。测试时并发量很小的时候可能不会存在问题(只是运气好),并发量一大就会有问题。

但是不是所有场景下并发使用map都是不安全的

这是golang的官方文档,上面提到了只要有更新的操作存在,map就是非线程安全的,但是如果使用场景只是并发读,不涉及到写/删操作,那么就是并发安全的。

image.png
image.png

源码分析

定义

map head中flags字段,记录了当前map的一些状态,其中hashWriting就是造成并发读写map报错的“罪魁祸首”。

代码语言:txt
AI代码解释
复制
// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}
代码语言:txt
AI代码解释
复制
	// flags
	iterator     = 1 // there may be an iterator using buckets
	oldIterator  = 2 // there may be an iterator using oldbuckets
	hashWriting  = 4 // a goroutine is writing to the map
	sameSizeGrow = 8 // the current map growth is to a new map of the same size

写入

  • 向map中新增元素最终会调用mapassign函数,在新增操作开始之前就会检验flagshashWriting位是否为1,为1则会报错。
  • 检验通过后会将该位置为1,标记当前正在写入。
  • 写入完成后将该位置为0
代码语言:txt
AI代码解释
复制
// Like mapaccess, but allocates a slot for the key if it is not present in the map.
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {

    ...
    
	if h.flags&hashWriting != 0 {
		throw("concurrent map writes")
	}
	hash := t.hasher(key, uintptr(h.hash0))

	// Set hashWriting after calling t.hasher, since t.hasher may panic,
	// in which case we have not actually done a write.
	h.flags ^= hashWriting

	...
done:
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	h.flags &^= hashWriting

    ...

读取

读取数据的过程相对简单,在读取之前判断是否有置位,校验通过则可以进行读操作,读操作时不会进行置位的。

这也是为啥,如果一个map被初始化ok之后,只要不做增删改,并发读报错的。

代码语言:txt
AI代码解释
复制
// mapaccess1 returns a pointer to h[key].  Never returns nil, instead
// it will return a reference to the zero object for the elem type if
// the key is not in the map.
// NOTE: The returned pointer may keep the whole map live, so don't
// hold onto it for very long.
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	...
	if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}
	...
}

结论

1.看过源码之后,发现这很像一个读写锁,但是并不会造成任何阻塞,有问题直接throw

2.如果真的有初始化一次之后,一直并发读的场景,可以大胆使用map。

常见解决方案

1.自己加锁读。

2.使用sync.map替代(看过一点原理,写数据时实现了加锁;使用了空间换时间的方式,用两个哈希结构存储Map,有一层缓存,加速读取数据。)

3.使用二维切片替代,将key和index做映射。

#如果有理解不到位或者理解失误的地方~欢迎指正~~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Golang map 三板斧第三式:实现原理
Go map 底层实现方式是 Hash 表(C++ map 基于红黑树实现,而 C++ 11 新增的 unordered_map 则与 Go map 类似,都是基于 Hash 表实现)。Go map 的数据被置入一个由桶组成的有序数组中,每个桶最多可以存放 8 个 key/value 对。key 的 Hash 值低位用于在该数组中定位到桶,而高 8 位则用于在桶中区分 key/value 对。
恋喵大鲤鱼
2020/11/12
7.5K0
Golang map 三板斧第三式:实现原理
map在golang的底层实现和源码分析
golang map底层由两个核心的结构体实现:hmap和bmap,bmap本篇用桶代替。
后端云
2022/11/25
2.1K1
map在golang的底层实现和源码分析
你不知道的Golang map
在开发过程中,map是必不可少的数据结构,在Golang中,使用map或多或少会遇到与其他语言不一样的体验,比如访问不存在的元素会返回其类型的空值、map的大小究竟是多少,为什么会报"cannot take the address of"错误,遍历map的随机性等等。 本文希望通过研究map的底层实现,以解答这些疑惑。 基于Golang 1.8.3
sunsky
2020/08/20
1.3K0
你不知道的Golang map
深度解密Go语言之map
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。
梦醒人间
2019/05/23
1.2K0
2万字图解map
上面引用的是维基百科对map的定义,意思是说,在计算机学科中,map是一种抽象的数据结构,它由key和value组成组成键值对的集合,在集合中每个key最多出现一次。像关联数组、符号表、字典数据结构都是map的一种具体实现 map数据结构在实际的项目使用的非常频繁,很多语言都提供了mpa数据结构,像Java语言的HashMap,Go语言中的map和sync.Map数据类型。map基本操作包含添加key和value键值对,获取key对应的value, 删除key,遍历操作。
数据小冰
2022/08/15
1.2K0
2万字图解map
深入理解 Go map:初始化和访问元素
从本文开始咱们一起探索 Go map 里面的奥妙吧,看看它的内在是怎么构成的,又分别有什么值得留意的地方?
李海彬
2019/05/08
1.5K0
深入理解 Go map:初始化和访问元素
深入Go的Map使用和实现原理
线性探测,字面意思就是按照顺序来,从冲突的下标处开始往后探测,到达数组末尾时,从数组开始处探测,直到找到一个空位置存储这个key,当数组都找不到的情况下回扩容(事实上当数组容量快满的时候就会扩容了);查找某一个key的时候,找到key对应的下标,比较key是否相等,如果相等直接取出来,否则按照顺寻探测直到碰到一个空位置,说明key不存在。如下图:首先存储key=xiaoming在下标0处,当存储key=xiaowang时,hash冲突了,按照线性探测,存储在下标1处,(红色的线是冲突或者下标已经被占用了) 再者key=xiaozhao存储在下标4处,当存储key=xiaoliu是,hash冲突了,按照线性探测,从头开始,存储在下标2处 (黄色的是冲突或者下标已经被占用了)
阿伟
2019/07/22
11.5K0
深入Go的Map使用和实现原理
由浅到深,入门Go语言Map实现原理
本篇文章主要以Map的读来展开分析,因为读弄明白了,其他的写、更新、删除等基本操作基本都可以猜出来了,不是么。
twelvecoder
2021/12/24
4570
由浅到深,入门Go语言Map实现原理
深入Go:sync.Map
我们在使用Go的项目中需要有并发读写的map时,我们了解到Go提供sync.Map这一数据结构;通过对其简单了解,发现它正好适合我们需要的场景。随着了解的深入,我们又有了疑惑:为什么不像Java SE 8之前的ConcurrentHashMap一样,使用分段锁?为什么在内部需要一个哨兵指针expunged?这两个问题我们简单Google后都没有找到解析和讨论,因此我们决定深入sync.Map的源代码,尝试回答这两个问题。
wenxing
2021/12/14
1.6K0
《Go小技巧&易错点100例》第四十篇
未恢复的 panic 会逐级向上传递,最终导致整个程序崩溃,所有协程(包括主协程)都会终止,崩溃表现:
闫同学
2025/07/14
1190
深入理解Go语言中的map
哈希表和数组是最常见的数据结构,几乎所有的语言都会有数组和哈希表两种容器类型 。哈希表表示的是键值对之间映射关系,在Go语言中,通过map来表示哈希表。 本文将深入浅出介绍map的概念、使用方式、底层结构、性能、最佳实现等话题,帮助开发更好的理解和使用map。
windealli
2024/03/21
3700
深入理解Go语言中的map
深入理解 Go map:赋值和扩容迁移
在 上一章节 中,数据结构小节里讲解了大量基础字段,可能你会疑惑需要 #&(!……#(!¥! 来干嘛?接下来我们一起简单了解一下基础概念。再开始研讨今天文章的重点内容。我相信这样你能更好的读懂这篇文章
李海彬
2019/05/08
2.7K0
深入理解 Go map:赋值和扩容迁移
如何设计并实现一个线程安全的 Map ?(上篇)
Map 是一种很常见的数据结构,用于存储一些无序的键值对。在主流的编程语言中,默认就自带它的实现。C、C++ 中的 STL 就实现了 Map,JavaScript 中也有 Map,Java 中有 HashMap,Swift 和 Python 中有 Dictionary,Go 中有 Map,Objective-C 中有 NSDictionary、NSMutableDictionary。
一缕殇流化隐半边冰霜
2018/08/30
2.2K0
如何设计并实现一个线程安全的 Map ?(上篇)
go map 原理与并发安全map
go map 整体和 java hashmap 差不多, 只是源码阅读的位置不太方便
leobhao
2024/11/29
1920
go map 原理与并发安全map
golang源码分析:go-json(1)
https://github.com/goccy/go-json起步比较晚,但是它大量参考了json-iterator/go的思路,同时也进行来一系列优化。它具体做了哪些优化呢,首先看下序列化:
golangLeetcode
2023/09/06
2930
golang源码分析:go-json(1)
码住!Golang并发安全与引用传递总结
导语 | 因为现在服务上云的趋势,业务代码都纷纷转向golang的技术栈。在迁移或使用的过程中,由于对golang特性的生疏经常会遇到一些问题,本文总结了golang并发安全和参数引用传值时的一些知识。 一、Map类型并发读写引发Fatal Error 先看一个在Go中关于Map类型并发读写的经典例子: var testMap = map[string]string{}func main() { go func() { for{ _ = testM
腾讯云开发者
2022/06/08
4460
码住!Golang并发安全与引用传递总结
由浅到深,入门Go语言Map实现原理
把自己学习知识进行一个总结。同时把一些可能困难、复杂难以理解的东西自我消化吸收后,简单化输出,降低他人的学习成本,提高他人的学习效率,主要为如下两点:
用户1093396
2020/12/23
9900
由浅到深,入门Go语言Map实现原理
golang源码分析:map
map 是由 key-value 对组成的;key 只会出现一次.主要的数据结构有两种:哈希查找表(Hash table)、搜索树(Search tree)。哈希查找表一般会存在“碰撞”的问题,就是说不同的 key 被哈希到了同一个 bucket。一般有两种应对方法:链表法和开放地址法。搜索树法一般采用自平衡搜索树,包括:AVL 树,红黑树。
golangLeetcode
2022/08/02
5080
理解Golang 赋值的并发安全性
比如对一个变量简单的自增操作count++,在非并发下很好理解,而在并发情况下却容易出现预期之外的结果,这样的代码就是非并发安全的。
sunsky
2023/03/08
9390
深入理解Go语言中的map:结构、性能与最佳实践
哈希表和数组是最常见的数据结构,几乎所有的语言都会有数组和哈希表两种容器类型 。哈希表表示的是键值对之间映射关系,在Go语言中,通过map来表示哈希表。本文将深入浅出介绍map的概念、使用方式、底层结构、性能、最佳实现等话题,帮助开发更好的理解和使用map。
windealli
2024/03/22
3.7K0
深入理解Go语言中的map:结构、性能与最佳实践
相关推荐
Golang map 三板斧第三式:实现原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档