在这个程序中,我们对map进行了并发的读和写,并且读和写的值并不一样,按照我们正常的逻辑来说,这个并发程序并没有问题,但是运行后却会发现出错了。 错误显示的是对map进行了并发的读和写。
为什么会出现这种问题呢?在map的扩容(具体的扩容过程自行百度)中,会产生新桶并且会逐步的删除旧桶,在并发的读写map时极易导致程序读到已删除的旧桶,go的创始人预见了这种情况,所以严格的限制了对map的并发读写。那么有没有办法解决这种情况呢?也有,最简单的办法就是在读写的时候加锁,但是这样在并发量大的情况下会严重影响运行速度。
这时就可以用到sync.map了。这边我们来看一下sync.map的源码,在sync包中的map.go中。
type Map struct { mu Mutex // read contains the portion of the map's contents that are safe for // concurrent access (with or without mu held). // // The read field itself is always safe to load, but must only be stored with // mu held. // // Entries stored in read may be updated concurrently without mu, but updating // a previously-expunged entry requires that the entry be copied to the dirty // map and unexpunged with mu held. read atomic.Value // readOnly // dirty contains the portion of the map's contents that require mu to be // held. To ensure that the dirty map can be promoted to the read map quickly, // it also includes all of the non-expunged entries in the read map. // // Expunged entries are not stored in the dirty map. An expunged entry in the // clean map must be unexpunged and added to the dirty map before a new value // can be stored to it. // // If the dirty map is nil, the next write to the map will initialize it by // making a shallow copy of the clean map, omitting stale entries. dirty map[any]*entry // misses counts the number of loads since the read map was last updated that // needed to lock mu to determine whether the key was present. // // Once enough misses have occurred to cover the cost of copying the dirty // map, the dirty map will be promoted to the read map (in the unamended // state) and the next store to the map will make a new dirty copy. misses int }
其中mu是互斥锁,read其实是个结构体,其源码为
m是map[any]*entry类型,any其实是个空接口,entry是个含有unsafe.pointer的结构体。所以它其实就是个能够存放任意类型key-value的map,amended就是修正的意思,如果脏映射包含一些不在m中的键,则为true。
回到sync.map中,dirty也是个能够存放任意类型key-value的map,而misses就是代表有没有命中的意思。
sync.map的整体结构就差不多是这个样子的
这里显示的是 m和dirty是共用一套key-value值的,那么有什么用呢?接下来我们在好好盘盘。
当我们要正常的读和修改时,我们只是走上面的这个m表。如图
当我们要追加数据时,会先正常的走一遍上面的表,然后发现表中没有这个key值,之后ameneded为true,再加上锁,这个锁是锁下面这个表的,上面这个表这时是可以正常访问的。之后走下面这个表,把键值对加上。
这时上面这个表是不完整的,但是读写时还是会先访问上面这个表,上面这个表的读和修改还是能做到并发的,只要你不涉及扩容就可以,扩容走下面这个表。
之后就是追加后读写的问题了,当上面这个表未命中,就会去读下面这个表(注意这是个互斥锁,当有其他人读写这个表时你就只能是等待了),如果下面这个表命中了,那么misses就会加1。
那你也不能总读下面这个表吧,因为上面表才是让你高频并发读写用的,当misses的值为len(dirty)时,就会触发dirty 提升,就会把上面这个表干掉,然后把下面这个表给提上去。然后amended改为false,misses改为0.
一开始并不会重建下面这个表,等到后来需要追加数据的时候才会重建下面这个表。这时就又开始新一轮的循环了。
接下来,我们来说说删除的问题,删除又分正常删除和追加后删除。
在正常删除的情况下,不删除key,就是把后面的Pointer置为nil,后面的value会因为gc过段时间就会被自动删除。
追加后删除:还是先从上面表开始找起,没找到之后将amended置为true,mu加锁。之后跟前面的一样,不删除key值,将Pointer置为nil。
当dirty提升的时候,这里的nil值也是跟着上去的。 但是在之后的重建dirty表时,并不会把nil值给复制下去,并把nil值置为expunged(也是已删除的意思)。
sync.map这里就介绍完毕了。sync.map解决了map扩容时的并发问题,做到了读写和扩容分离。