背景
最近翻看之前的代码,发现使用了 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,非常适合读多写少的情况。
本文探讨了Go语言中的sync.Map在并发环境下的工作原理,通过源码分析揭示了其读写分离的设计,以及在Range和Store操作并发时可能的结果。测试结果显示,Range可能在特定情况下遍历到Store新增的数据,这取决于两者对锁的获取顺序。sync.Map的设计旨在确保并发安全并优化读取性能,特别适合读多写少的场景。
182

被折叠的 条评论
为什么被折叠?



