教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Golang map 并发读写问题源码分析

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

发布时间:2021-12-25   编辑:jiaochengji.com
教程集为您提供Golang map 并发读写问题源码分析等资源,欢迎您收藏本站,我们将为您提供最新的Golang map 并发读写问题源码分析资源
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"/></svg><h2>map介绍及问题描述</h2>

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

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

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

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

<h2>
源码分析</h2> <h3>定义</h3>

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

<pre><code>// 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 } </code></pre> <pre><code> // 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 </code></pre> <h3>
写入</h3> <ul><li>向map中新增元素最终会调用<code>mapassign</code>函数,在新增操作开始之前就会检验<code>flags</code>的<code>hashWriting</code>位是否为1,为1则会报错。</li><li>检验通过后会将该位置为1,标记当前正在写入。</li><li>写入完成后将该位置为0</li></ul><pre><code>// 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 ... </code></pre> <h3>读取</h3>

读取数据的过程相对简单,在读取之前判断是否有置位,校验通过则可以进行读操作,读操作时不会进行置位的。
这也是为啥,如果一个map被初始化ok之后,只要不做增删改,并发读报错的。

<pre><code>// 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") } ... } </code></pre> <h2>
结论</h2>

1.看过源码之后,发现这很像一个读写锁,但是并不会造成任何阻塞,有问题直接<code>throw</code>。
2.如果真的有初始化一次之后,一直并发读的场景,可以大胆使用map。

<h2>
常见解决方案</h2>

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

到此这篇关于“Golang map 并发读写问题源码分析”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang map 并发读写问题源码分析
Golang的map并发安全
golang 并发访问map遇到的问题
golang key map 所有_谨慎使用golang中的map
go二维map_Golang使用Map的正确姿势
Go 回答汇总三
golang中map的一些注意事项
golang map并发读写问题踩坑记录 `concurrent map read and map write`
go基于viper实现配置文件热更新及其源码分析
golang map 锁_golang中线程安全的map

[关闭]
~ ~