「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」前端
咱们都知道go的map是并发不安全的,当几个goruotine同时对一个map进行读写操做时,就会出现并发写问题fatal error: concurrent map writes
后端
理论上只要在多核cpu下,若是子goroutine和主gouroutine同时在运行,就会出现问题。咱们不妨用go自带的-race
来检测下,能够运行 go run -race main.go
安全
经过检测,咱们能够发现,存在
data race
,即数据竞争问题。有人说这简单,加锁解决,加锁当然能够解决,可是你懂的,锁的开销问题。
撇开数据竞争的问题,咱们能够经过看个例子来了解下锁的开销:markdown
BenchmarkAddMapWithUnLock
是测试无锁的BenchmarkAddMapWithLock
是测试有锁的经过go test -bench .
来跑测试,得出的结果以下:并发
能够发现无锁的平均耗时约
6.6 ms
,带锁的平均耗时约7.0 ms
,虽然说相差无几,但也反应加锁的开销。在一些复杂的案例中,可能会更明显。源码分析
有人说,既然锁开销大,那么就用go内置的方法sync.map,它能够解决并发问题。sync.map确实能够解决并发map问题,可是它在读多写少的状况下,比较适合,能够保证并发安全,同时又不须要锁的开销,在写多读少的状况下反而可能会更差,主要是由于它的设计,咱们从源码分析看看:post
read的数据存在readOnly.m中,也是个map,value是个entry的指针,entry是个结构体具体类型以下:性能
里面就一个p,当咱们设置一个key的value时,能够理解为p就是指向这个value的指针(p就是value的地址)。
当readOnly.amended = true
的时候,表示read的数据不是最新的,dirty里面包含一些read没有的新key。
3. Map的dirty也是map类型,从命名来看它是脏的,能够理解某些场景新加kv的时候,会先加到dirty中,它比read要新。
4. Map的misses,当从read中没读到数据,且amended=true的时候,会尝试从dirty中读取,而且misses会加1,当misssed数量大于等于dirty的长度的时候,就会把dirty赋给read,同时重置missed和dirty。测试
sync.map的核心思想就是空间换时间。
假设如今有个画展对外展现(read
)n幅画,一群人来看,你们在这个画展上想看什么就看什么,不用等待、不用排队。这时上了副新画,可是因为画展示在在工做时间,不能直接挂上去,并且新画可能还要保养什么,暂时不放在画展(read
)上,因而就先放在备份的仓库中(dirty
),若是真有人要看这幅新画,那么只能领他到仓库中(dirty
)中去看,假设这时来了个新画,此时仓库中有n+1副画了,这时有人来问:有没有这幅新画呀,经理说:有,你和我到仓库中去看下。这时又有人来问:有没有这幅新画呀,经理说:有,你和我到仓库中去看下。当问有没有这幅新画的次数达到了n+1的时候,这时画展的老板发现这幅新画要看的人还很多。因而对经理说:你去看下,等下没人看画展(read
)的时候,把画展(read
)的画所有下掉,把仓库(dirty)里面的画所有换上。当经理所有换结束后,此时画展(read
)上已是最全最新的画了。
sync.map的原理大概就相似上面的例子,在少许人对新画(新的k、v
)感兴趣的时候,就带他去仓库(dirty
)看,此时由于经理只有一个,因此每次只能带一我的(加锁
),效率低,其余的画,在画展(read
)上,随便看,效率高。atom
tryStore里面有判断
p == expunged
就返回false。p有三种类型:nil
(read中的key被delete的时候其实软删除,只是把p设置成nil)、expunged
(被删除的key(p==nil)会在read copy 到 dirty的时候再被设置成expunged)、其余正常的value的地址
,这里若是是expunged就不选择更新value。
而后在dirty中设置新的k、v。(这里能够发现新的k、v都是先加在dirty的map中的,read是没有的)。
6. 如今dirty是比较干净的数据了(已经清空了nil或expunged的key),设置amended=true(说明此时dirty不为空,且dirty中有新数据)
7. 解锁
总结:
if else
的分支判断。总体确定是比常规map加锁性能要差的。 6. 若是没有对应的key,就返回nil,有的话,就返回对应的value
总结:
总结:当删除的key在read中,能够经过软删除来标记,这样自己read对应的map不会由于频繁删除而触发等量扩容,关于map的扩容规则能够参考map原理。
经过分析了sync.map咱们发现,在读多写少的状况下,仍是比较优秀的,相比常规map加锁那种确定是更好的,可是写多读少的状况下,并不适合,由于仍是涉及到频繁的加锁、read和dirty交换等开销,搞很差还比常规的map加锁性能更差。咱们仍是经过一个极端的例子来看:
BenchmarkAddMapWithUnLock
是测试无锁的BenchmarkAddMapWithLock
是测试有锁的BenchmarkAddMapWithSyncMap
是测试sync.map3个方法都是对一个map加10w条数据。
经过go test -bench .
来跑测试,得出的结果以下:
能够看出sync.map的耗时是其余的两个的5倍左右。sync.map是个好东西,可是场景用错,反而拔苗助长。