map介绍及问题描述
map主要用来存储kv数据,其底层使用的是开链法去冲突的hashtable,拥有自动扩容机制。使用map最方便的一点是可以O(1)快速查询(目前slice并没有提供查询接口,只能通过自己写算法实现某个元素是否存在)。
map虽然好用,但是可能不适用。
但是map有一个非常致命的坑点,在并发场景下,并发读/写都可能会出现fatal error:concurrent map read and map write
的错误,刚开始使用map的时候天真的认为只要不对同一个key进行并发操作就行,但是现实很骨感。测试时并发量很小的时候可能不会存在问题(只是运气好),并发量一大就会有问题。
但是不是所有场景下并发使用map都是不安全的
这是golang的官方文档,上面提到了只要有更新的操作存在,map就是非线程安全的,但是如果使用场景只是并发读,不涉及到写/删操作,那么就是并发安全的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwYcv6EP-1597712982988)(https://yunpan.oa.tencent.com/note/api/file/getImage?fileId=5f33ebf86f0b9316e203d9bc)]
源码分析
定义
map head中flags字段,记录了当前map的一些状态,其中hashWriting
就是造成并发读写map报错的“罪魁祸首”。
// 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
}
// 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
函数,在新增操作开始之前就会检验flags
的hashWriting
位是否为1,为1则会报错。 - 检验通过后会将该位置为1,标记当前正在写入。
- 写入完成后将该位置为0
// 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之后,只要不做增删改,并发读报错的。
// 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做映射。
#如果有理解不到位或者理解失误的地方欢迎指正~