最近在项目中需要在多线程下操作map,查阅资料后发现golang得sync包中有提供一个sync.map可以作为线程安全得map使用,但是最后同时推荐了另一个开源的cmap工具包,性能较sync.map更出色,所以没有使用golang的syanc包的map。在这里对两种map进行一下总结和学习。

1.sync.map

golang中如果在多个routine中使用map,是有可能会发生fatal错误导致程序挂掉的。所以在sync包中提供了一个线程安全的map,用Load和Store方法来代替对普通map的setget一般多routine中操作map都是加互斥锁,但枷锁在map操作频繁的时候性能会很差,而且写会阻塞读在sync.map中的思路是用空间换时间,先看下sync.map中主要的数据结构

mu是在阻塞操作时加的锁,read和dirty是两个map,可以把read理解为dirty的缓存(之后会详解sync.map中的读操作),且需要注意read和dirty中存储的value都是指针;misses是一个int值,用于记录在read中没有读到但在dirty中存在的值的次数(缓存未命中)且在misses值到一个阈值之后会发生一次dirty转换为read(后面简称dr转换);map中的value有两个特殊值expunged和nil,跟值的删除有关,之后再进行解释

首先看它的Load方法

Load方法比较简单,用于从map中获取对应key的value,主要逻辑是先从read中查找key,如果read中没有找到,再去dirty中找,并调用missLocked方法使misses值加一,missLocked方法中如果misses值大于dirty的长度后,会进行一次dr转换。

注意:

a.这里用到了加锁双重检查,具体原因后面再解释,sync.Map中很多地方都用了这个。

b.amended是一个bool值,当dirty中有read中没有的kv对时为true,只有在dr转换发生后且没有新的kv store进来,dirty为nil,才会为false

然后是Store方法,用于存储kv

在调用Store方法时有两种情况,一种是新增kv,一种是替换一个key的旧value值。如果是进行值的替换,sync.Map在进行值的替换的时候,先是直接在read中进行替换,因为前面说过read和dirty中存储的都是指针,所以在read中替换值,dirty也是可见的。如果替换失败(该key在read中的value为expunged)或是read中没有找到这个key,则进入下面的逻辑。首先如果确定该key在read中的value为expunged,则重新在dirty新增该kv,并将v进行替换;如果是在read中没有这个key,但dirty中有,说明这个kv是新增到dirty的且之后未发生dr转换,此时在dirty中对旧v进行替换;如果dirty中也没有这个kv,先判断是否刚进行过一次dr转换(判断amended的值),如果是则调用dirtylocked方法,然后在dirty中新增kv

dirtylocked方法将read中的v不为nil的kv复制到dirty中,read为nil的kv(后面可以知道这是被delete掉的kv)则将v设置为expunged,在下次dr转换时即被永久删除。

最后再看看Delete方法,调用Delete后先看在read中是否有该kv,如果有,就调用e.delete方法进行删除,如果没有再看dirty中是否存在该kv,如果有则用golang的delete方法从dirty中删除

e.delete方法是是惰性删除,调用后将read中该kv的v设置为nil。

2.cmap

这个map是leader让我去了解的,是一个github上的开源项目,地址是https://github.com/fanliao/go-concurrentMap,对比golang中自带的线程安全map,这种map的实现方式比较简单,还是通过加锁来解决多routine操作map的问题。但是和sync.Map不同的是,cmap使用了分段锁的方式,先看下cmap主要的数据结构

cmap其实就是一个ConcurrentMapShared结构体的切片,而每一个ConcurrentMapShared结构体都单独维护一个互斥锁。

可以看下cmap的Get方法:

Get方法很简单,主要逻辑就是先通过GetShard方法定位到应该去哪个shard上查找key,然后在该shard上加锁并读取value,getShard方法也很简单,就是简单的进行求哈希运算

而cmap的set,has等方法都是在原生map的基础上加了个分段锁操作,实现逻辑非常简单,感兴趣的话可以再自行去看源码了解。

3.总结

由于没有足够的测试,下面的观点仅为个人观点。仅从实现方式上看,个人认为在map读多写少,kv更新不频繁且数据量不大的情况下使用sync.Map就足够。