map介绍及问题描述

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

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

fatal error:concurrent map read and map write

但是不是所有场景下并发使用map都是不安全的
这是golang的官方文档,上面提到了只要有更新的操作存在,map就是非线程安全的,但是如果使用场景只是并发读,不涉及到写/删操作,那么就是并发安全的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwYcv6EP-1597712982988)(https://yunpan.oa.tencent.com/note/api/file/getImage?fileId=5f33ebf86f0b9316e203d9bc)]

源码分析

定义

hashWriting
// 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

写入

mapassignflagshashWriting
// 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")
	}
	...
}

结论

throw

常见解决方案

1.自己加锁读。
2.使用sync.map替代(看过一点原理,写数据时实现了加锁;使用了空间换时间的方式,用两个哈希结构存储Map,有一层缓存,加速读取数据。)
3.使用二维切片替代,将key和index做映射。
#如果有理解不到位或者理解失误的地方欢迎指正~