前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang map 并发读写问题源码分析

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

原创
作者头像
Lynalmost
发布2022-06-27 17:13:22
1.1K0
发布2022-06-27 17:13:22
举报

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
复制
// 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
复制
	// 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
复制
// 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
复制
// 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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • map介绍及问题描述
  • 源码分析
    • 定义
      • 写入
        • 读取
        • 结论
        • 常见解决方案
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档