背景
最近翻看之前的代码,发现使用了 sync.Map
,并对其异步做了 Store
和 Range
的操作。
Range
和 Store
异步,能够遍历到后添加的数据吗?带着这个问题,翻了下源码,简单了解其原理。先说结论:可能会遍历到 Store
添加的数据的。
查看本篇文章时,最好对 sync.Map
的源码有个初步的了解。
测试代码
先来看一段代码,输出什么?
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
type cans struct {
t int
v any
}
var c = make(chan cans, 10000)
m := new(sync.Map)
m.Store("a", "a")
m.Store("b", "b")
m.Store("c", "c")
var ccc = make(chan struct{})
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
if i == 1 {
ccc <- struct{}{}
}
m.Store(i, i)
c <- cans{1, i}
}
fmt.Println("store end")
}()
wg.Add(1)
go func() {
<-ccc
defer wg.Done()
// range 的时候会判断是否有 dirty 数据,有的话也会去竞争锁,当拿到锁的那一刻,会清空 dirty 同步 read 然后会进入 for 去回调
m.Range(func(key, value any) bool {
c <- cans{2, key}
return true
})
fmt.Println("range end")
}()
wg.Wait()
close(c)
fmt.Println("channel 长度:", len(c))
for v := range c {
fmt.Println(v.t, ": ", v.v)
}
fmt.Println("end")
}
测试结果:Range
输出的结果是包含了一部分另一个协程中 Store
进入的数据的。
源码解读
- 通过
read
和dirty
两个字段将读写分离,读的数据存在只读字段read
上,将最新写入的数据则存在dirty
字段上 - 读取时会先查询
read
,不存在再查询dirty
,写入时则只写入dirty
- 读取
read
并不需要加锁,而读或写dirty
都需要加锁 - 另外有
misses
字段来统计read
被穿透的次数(被穿透指需要读dirty
的情况),超过一定次数则将dirty
数据同步到read
上 - 对于删除数据则直接通过标记来延迟删除
Range
遍历时会先判断dirty
中是否有read
中不存在的数据,有则加锁,同步至read
后解锁 ,遍历当前的read
sync.Map
的数据结构
type Map struct {
// 加锁作用,保护 dirty 字段
mu Mutex
// 只读的数据,实际数据类型为 readOnly
read atomic.Value
// 最新写入的数据
dirty map[interface{}]*entry
// 计数器,每次需要读 dirty 则 +1
misses int
}
readOnly
数据结构
type readOnly struct {
// 内建 map
m map[interface{}]*entry
// 表示 dirty 里存在 read 里没有的 key,通过该字段决定是否加锁读 dirty
amended bool
}
entry
数据结构则用于存储值的指针
type entry struct {
p unsafe.Pointer // 指针
}
总结
再详细解释一下开头提出的问题,Range
Store
并发的时候,Range
的遍历结果如何。
Range
先开始,并且没有发现dirty
中有read
中不存在的数据(即:m.read.amended
为false
),则遍历过程中是不能够获取到新添加进Map
中的数据的,因为遍历的是之前的副本。Store
先开始,Range
遍历的时候会发现dirty
中有read
中不存在的数据,则会去和Store
去竞争同一把锁,在Range
竞争到锁的时候,会清空dirty
同步read
然后会拿着当前read
的副本进入for
去回调,即:遍历过程中可能会读取到Store
的一部分的数据。
Range
的时候能不能获取到新 Store
的数据,主要是看谁先获取到锁。若是 Store
先获取到,则 Range
的时候还是能遍历到的,若是 Range
先获取到锁,则只会遍历当前 Map
中已有的数据。
sync.Map
的读写分离设计,解决了并发情况的写入安全,又使读取速度在大部分情况可以接近内建 map
,非常适合读多写少的情况。