Golang中存在2种map, 一种是常规的非线程安全map,另一种是线程安全的sync.map
。在golang1.9以前,我们在项目中是通过数组、map、sync.RWMutex来实现普通map的并发读写(采用map数组,把key hash到相应的map,每个map单独加锁以降低锁的粒度),那么golang sync.map
是如何实现线程安全的呢?
一、原理
通过2个map(read、dirty)来实现读写分离,read、dirty 指向同一个entity。
- 读数据(Load):首先从read map中读(无锁),如果不存在,则到dirty map中读(加锁)。
- 存数据(Store):首先查找数据是否在read map中,如果在则更新entity.p(无锁);否则在dirty map中查找,如果存在则更新,不存在则新建key(有锁)。
- 删除数据(Delete):如果数据在read map中找到,则直接把
entity.p
置为nil(无锁);如果read map中不存在则在dirty map中查找,若找到则直接删除键值对(有锁)。之所以read map只置为nil:1.因为read map是无锁访问的,不能直接删除键值对;2.为了延迟删除 - dirty map提拔为read map
在read map中访问数据时,当miss次数>=len(m.dirty)时,会把dirty map提拔为read map(直接修改read map的指针指向dirty map), 并把dirty map置为nil。 - 延迟删除
删除数据时,如果在read map中找到,并不会直接删除键值对;而是把value指向的entity.p
置为nil,并在下次构建dirty map时把值为nil的p设为expunged。若read map中值为expunged的键值对若在下次提拔dity map时还没有写入数据,则会在被彻底删除。(因为提拔的方式是dity直接覆盖read,dity中没有,新的read中也不会有)
从上面可以看出:操作read map中的数据,无论是读(Load)、存(Store, 如果key在read map中,则直接更新entity,否则转为修改dirty)、删除(Delete,同Store)都不需要加锁(通过CAS(自旋转锁)实现 参考),但对dirty map的所有操作都是加锁的。下面结合源码来看它是如何实现的。
二、源码
1.1 sync.map
结构体定义
1 | //go1.12.7 |
1.2 关键函数
1 | //如果存在key,则返回对应的value, 如果不存在返回nil, ok表示是否找到值 |
三、适用场景
从源码可以看出sync.map
的优点和缺点都很明显:
- 优点:
1)并发读时,若数据存在read map中,则不需要加锁。
2)并发写时,若只更新read map中entry指向的值,不需要加锁 - 缺点:
频繁访问read map中不存在的值时(不管在dirty map中是否存在),不但要加锁而且还会加快dirty map提拔为read map,并在下次构建dirty map时拷贝read map中的非空值。
这种场景可以用数组+map+mutex的方式来代替。例如