首页>>后端>>Golang->Golang sync.Map是如何做到尽量避免使用锁?

Golang sync.Map是如何做到尽量避免使用锁?

时间:2023-11-29 本站 点击:0

sync.Map就像Go中的原生map,但是是线程安全的在没有锁或者协调的多goroutines中。加载,存储和删除以摊销的固定时间运行。

Map类型针对两种常见用例进行了优化:(1)给定键的条目仅写入一次但多次读取(如仅在高速缓存中的高速缓存中),或(2)当多个goroutine进行读取,写入和覆盖不相交的键集的条目。在这两种情况下,与单独的Mutex或RWMutex配对的Go map相比,使用sync.Map可以显着减少锁争用。 一个Map在首次使用后,禁止拷贝。

typeMapstruct{muMutex//readcontainstheportionofthemap'scontentsthataresafefor//concurrentaccess(withorwithoutmuheld).//read包含map内容中可以并行访问的部分内容//Thereadfielditselfisalwayssafetoload,butmustonlybestoredwith//muheld.//read字段自身是可以安全加载的,但是在装载的时候必须使用锁//Entriesstoredinreadmaybeupdatedconcurrentlywithoutmu,butupdating//apreviously-expungedentryrequiresthattheentrybecopiedtothedirty//mapandunexpungedwithmuheld.//可以不使用mu同时更新以read形式存储的条目,//但是更新以前删除的条目要求将该条目复制到脏映射,并且在保留mu的情况下不删除它。readatomic.Value//readOnly//dirty包含需要mu保护的map内容,为了确保可以将脏映射快速提升到读取映射,它还包括read//map中的所有未删除条目。//dirtycontainstheportionofthemap'scontentsthatrequiremutobe//held.Toensurethatthedirtymapcanbepromotedtothereadmapquickly,//italsoincludesallofthenon-expungedentriesinthereadmap.//已删除条目不存在dirtymap中,//必须先清除干净映射中的已删除条目并将其添加到脏映射中,然后才能将新值存储到脏映射中。//Expungedentriesarenotstoredinthedirtymap.Anexpungedentryinthe//cleanmapmustbeunexpungedandaddedtothedirtymapbeforeanewvalue//canbestoredtoit.//如果脏映射为nil,则对映射的下一次写入将通过创建干净映射的浅表副本来初始化它,并忽略陈旧的条目。//Ifthedirtymapisnil,thenextwritetothemapwillinitializeitby//makingashallowcopyofthecleanmap,omittingstaleentries.dirtymap[interface{}]*entry//missescountsthenumberofloadssincethereadmapwaslastupdatedthat//neededtolockmutodeterminewhetherthekeywaspresent.//misses计数因为readmap上次更新导致需要锁来决定key是否存在//Onceenoughmisseshaveoccurredtocoverthecostofcopyingthedirty//map,thedirtymapwillbepromotedtothereadmap(intheunamended//state)andthenextstoretothemapwillmakeanewdirtycopy.//一旦到了足够的次数就触发将dirtymap转化为readmap,当再有store时,重新生成一个dirtymapmissesint}//readOnlyisanimmutablestructstoredatomicallyintheMap.readfield.typereadOnlystruct{mmap[interface{}]*entryamendedbool//trueifthedirtymapcontainssomekeynotinm.}//expungedisanarbitrarypointerthatmarksentrieswhichhavebeendeleted//fromthedirtymap.//标记元素删除varexpunged=unsafe.Pointer(new(interface{}))//Anentryisaslotinthemapcorrespondingtoaparticularkey.typeentrystruct{//ppointstotheinterface{}valuestoredfortheentry.////Ifp==nil,theentryhasbeendeletedandm.dirty==nil.////Ifp==expunged,theentryhasbeendeleted,m.dirty!=nil,andtheentry//ismissingfromm.dirty.////Otherwise,theentryisvalidandrecordedinm.read.m[key]and,ifm.dirty//!=nil,inm.dirty[key].////Anentrycanbedeletedbyatomicreplacementwithnil:whenm.dirtyis//nextcreated,itwillatomicallyreplacenilwithexpungedandleave//m.dirty[key]unset.////Anentry'sassociatedvaluecanbeupdatedbyatomicreplacement,provided//p!=expunged.Ifp==expunged,anentry'sassociatedvaluecanbeupdated//onlyafterfirstsettingm.dirty[key]=esothatlookupsusingthedirty//mapfindtheentry.punsafe.Pointer//*interface{}}funcnewEntry(iinterface{})*entry{return&entry{p:unsafe.Pointer(&i)}}

read这个原生字典可以被看作一个快照,它总会在条件满足时,去重新保存所属的sync.Map值中包含的所有键值对。read字典虽然不会增减其中的键,但却允许变更其中的键所对应的值。所以,它并不是传统意义上的快照,它的只读特性只是对于其中键的集合而言的。

dirty字段代表的原生字典,它存储键值对的方式与read字段中的原生字典一致,它的键类型也是interface{},并且同样是把值先做转换和封装后再进行储存的。我们暂且把它称为脏字典。

注意,脏字典和只读字典如果都存有同一个键值对,那么这里的两个键指的肯定是同一个基本值,对于两个值来说也是如此。

正如前文所述,这两个字典在存储键和值的时候都只会存入它们的某个指针,而不是基本值。

sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。

//Storesetsthevalueforakey.func(m*Map)Store(key,valueinterface{}){read,_:=m.read.Load().(readOnly)ife,ok:=read.m[key];ok&&e.tryStore(&value){return}m.mu.Lock()read,_=m.read.Load().(readOnly)ife,ok:=read.m[key];ok{ife.unexpungeLocked(){//Theentrywaspreviouslyexpunged,whichimpliesthatthereisa//non-nildirtymapandthisentryisnotinit.m.dirty[key]=e}e.storeLocked(&value)}elseife,ok:=m.dirty[key];ok{e.storeLocked(&value)}else{if!read.amended{//We'readdingthefirstnewkeytothedirtymap.//Makesureitisallocatedandmarktheread-onlymapasincomplete.m.dirtyLocked()m.read.Store(readOnly{m:read.m,amended:true})}m.dirty[key]=newEntry(value)}m.mu.Unlock()}

相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。否则,它才会在锁的保护下把键值对存储到脏字典中。这个时候,该键值对的“已删除”标记会被抹去。

func(e*entry)load()(valueinterface{},okbool){p:=atomic.LoadPointer(&e.p)ifp==nil||p==expunged{returnnil,false}return*(*interface{})(p),true}

顺便说一句,只有当一个键值对应该被删除,但却仍然存在于只读字典中的时候,才会被用标记为“已删除”的方式进行逻辑删除,而不会直接被物理删除。

//Deletedeletesthevalueforakey.func(m*Map)Delete(keyinterface{}){read,_:=m.read.Load().(readOnly)e,ok:=read.m[key]if!ok&&read.amended{m.mu.Lock()read,_=m.read.Load().(readOnly)e,ok=read.m[key]if!ok&&read.amended{delete(m.dirty,key)}m.mu.Unlock()}ifok{e.delete()}}func(e*entry)delete()(hadValuebool){for{p:=atomic.LoadPointer(&e.p)ifp==nil||p==expunged{returnfalse}ifatomic.CompareAndSwapPointer(&e.p,p,nil){returntrue}}}

这种情况会在重建脏字典以后的一段时间内出现。不过,过不了多久,它们就会被真正删除掉。

在查找和遍历键值对的时候,已被逻辑删除的键值对永远会被无视。对于删除键值对,sync.Map会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。

最后,sync.Map会把该键值对中指向值的那个指针置为nil,这是另一种逻辑删除的方式。

除此之外,还有一个细节需要注意,只读字典和脏字典之间是会互相转换的。

func(m*Map)missLocked(){m.misses++ifm.misses<len(m.dirty){return}m.read.Store(readOnly{m:m.dirty})m.dirty=nilm.misses=0}

在脏字典中查找键值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read字段中,然后把代表脏字典的dirty字段的值置为nil。

在这之后,一旦再有新的键值对存入,它就会依据只读字典去重建脏字典。这个时候,它会把只读字典中已被逻辑删除的键值对过滤掉。理所当然,这些转换操作肯定都需要在锁的保护下进行。

综上所述,sync.Map的只读字典和脏字典中的键值对集合,并不是实时同步的,它们在某些时间段内可能会有不同。

由于只读字典中键的集合不能被改变,所以其中的键值对有时候可能是不全的。

相反,脏字典中的键值对集合总是完全的,并且其中不会包含已被逻辑删除的键值对。

因此,可以看出,在读操作有很多但写操作却很少的情况下,并发安全字典的性能往往会更好。

在几个写操作当中,新增键值对的操作对并发安全字典的性能影响是最大的,

其次是删除操作,

最后才是修改操作。

如果被操作的键值对已经存在于sync.Map的只读字典中,并且没有被逻辑删除,那么修改它并不会使用到锁,对其性能的影响就会很小。

内容来源:

部分内容摘自郝林老师的极客时间

作者:第八共同体


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Golang/257.html