Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
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.4K0
Golang map 三板斧第三式:实现原理
map在golang的底层实现和源码分析
golang map底层由两个核心的结构体实现:hmap和bmap,bmap本篇用桶代替。
后端云
2022/11/25
2.1K1
map在golang的底层实现和源码分析
go map 原理与并发安全map
go map 整体和 java hashmap 差不多, 只是源码阅读的位置不太方便
leobhao
2024/11/29
1540
go map 原理与并发安全map
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.1K0
2万字图解map
深入理解 Go map:赋值和扩容迁移
在 上一章节 中,数据结构小节里讲解了大量基础字段,可能你会疑惑需要 #&(!……#(!¥! 来干嘛?接下来我们一起简单了解一下基础概念。再开始研讨今天文章的重点内容。我相信这样你能更好的读懂这篇文章
李海彬
2019/05/08
2.6K0
深入理解 Go map:赋值和扩容迁移
深入理解 Go map:初始化和访问元素
从本文开始咱们一起探索 Go map 里面的奥妙吧,看看它的内在是怎么构成的,又分别有什么值得留意的地方?
李海彬
2019/05/08
1.4K0
深入理解 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.1K0
如何设计并实现一个线程安全的 Map ?(上篇)
golang源码分析:map
map 是由 key-value 对组成的;key 只会出现一次.主要的数据结构有两种:哈希查找表(Hash table)、搜索树(Search tree)。哈希查找表一般会存在“碰撞”的问题,就是说不同的 key 被哈希到了同一个 bucket。一般有两种应对方法:链表法和开放地址法。搜索树法一般采用自平衡搜索树,包括:AVL 树,红黑树。
golangLeetcode
2022/08/02
4910
深度解密Go语言之map
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。
梦醒人间
2019/05/23
1.2K0
你不知道的Golang map
在开发过程中,map是必不可少的数据结构,在Golang中,使用map或多或少会遇到与其他语言不一样的体验,比如访问不存在的元素会返回其类型的空值、map的大小究竟是多少,为什么会报"cannot take the address of"错误,遍历map的随机性等等。 本文希望通过研究map的底层实现,以解答这些疑惑。 基于Golang 1.8.3
sunsky
2020/08/20
1.2K0
你不知道的Golang map
由浅到深,入门Go语言Map实现原理
把自己学习知识进行一个总结。同时把一些可能困难、复杂难以理解的东西自我消化吸收后,简单化输出,降低他人的学习成本,提高他人的学习效率,主要为如下两点:
用户1093396
2020/12/23
9590
由浅到深,入门Go语言Map实现原理
由浅到深,入门Go语言Map实现原理
本篇文章主要以Map的读来展开分析,因为读弄明白了,其他的写、更新、删除等基本操作基本都可以猜出来了,不是么。
twelvecoder
2021/12/24
4330
由浅到深,入门Go语言Map实现原理
《Go小技巧&易错点100例》第四十篇
未恢复的 panic 会逐级向上传递,最终导致整个程序崩溃,所有协程(包括主协程)都会终止,崩溃表现:
闫同学
2025/07/14
680
同样作为非并发安全的数据结构,slice和map在有并发安全问题时,为什么表现相差那么大
Because some cases are easy and cheap to detect for maps, but there
fliter
2023/06/18
2530
同样作为非并发安全的数据结构,slice和map在有并发安全问题时,为什么表现相差那么大
深入理解Go语言中的map:结构、性能与最佳实践
哈希表和数组是最常见的数据结构,几乎所有的语言都会有数组和哈希表两种容器类型 。哈希表表示的是键值对之间映射关系,在Go语言中,通过map来表示哈希表。本文将深入浅出介绍map的概念、使用方式、底层结构、性能、最佳实现等话题,帮助开发更好的理解和使用map。
windealli
2024/03/22
3.1K0
深入理解Go语言中的map:结构、性能与最佳实践
码住!Golang并发安全与引用传递总结
导语 | 因为现在服务上云的趋势,业务代码都纷纷转向golang的技术栈。在迁移或使用的过程中,由于对golang特性的生疏经常会遇到一些问题,本文总结了golang并发安全和参数引用传值时的一些知识。 一、Map类型并发读写引发Fatal Error 先看一个在Go中关于Map类型并发读写的经典例子: var testMap = map[string]string{}func main() { go func() { for{ _ = testM
腾讯云开发者
2022/06/08
4280
码住!Golang并发安全与引用传递总结
go 高并发下的数据结构是怎样?
0 字节的变量在内存中的地址是相同的,称为 zerobase,这个变量在 runtime/malloc.go 文件中
阿珍
2024/07/21
960
go 高并发下的数据结构是怎样?
深入理解Go语言中的map
哈希表和数组是最常见的数据结构,几乎所有的语言都会有数组和哈希表两种容器类型 。哈希表表示的是键值对之间映射关系,在Go语言中,通过map来表示哈希表。 本文将深入浅出介绍map的概念、使用方式、底层结构、性能、最佳实现等话题,帮助开发更好的理解和使用map。
windealli
2024/03/21
3000
深入理解Go语言中的map
go哈希
哈希查找表一般会存在“碰撞”的问题,就是说不同的 key 被哈希到了同一个 bucket。一般有两种应对方法:链表法和开放地址法。链表法将一个 bucket 实现成一个链表,落在同一个 bucket 中的 key 都会插入这个链表。开放地址法则是碰撞发生后,通过一定的规律,在数组的后面挑选“空位”,用来放置新的 key。
Michel_Rolle
2023/11/06
3.1K1
go 为什么要有new 和 make
应用类型本质其实拿到的是一个指针,指针的零值是nil, 所以如果不显式声明,是不会自动分配内存的。
solate
2022/05/09
8850
相关推荐
Golang map 三板斧第三式:实现原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档