是什么
sync.Map
是 Go 语言标准库 sync
包中的一个线程安全的映射数据结构,用于在多个 goroutine 之间安全地存储和检索键值对
怎么用
package main
import (
"fmt"
"sync"
)
func main() {
// 创建一个 sync.Map
var m sync.Map
// 向 sync.Map 中存储键值对
m.Store("key1", "value1")
m.Store("key2", "value2")
// 从 sync.Map 中检索值
val, ok := m.Load("key1")
if ok {
fmt.Println("Value for key1:", val)
}
// 删除键值对
m.Delete("key2")
// 检查键是否存在
_, ok = m.Load("key2")
if !ok {
fmt.Println("Key2 not found")
}
// 使用 Range 迭代所有键值对
m.Range(func(key, value interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", key, value)
return true // 返回 true 继续迭代,返回 false 停止迭代
})
}
为什么(解析)
我们首先声明一个该类型的变量,那么这个变量底层的数据结构是如何呢?
sync.Map的数据结构如下
type Map struct{
mu Mutext //dirty操作的时候需要使用这个锁
read atomic.Value //只读的数据
dirty map[interface{}]*entry//新写的值都放在dirty里,和read有冗余的嫌疑
misses int//read没命中值的次数
}
read的数据结构是
type readOnly struct{
m map[interface{}]*entry
amended bool // 是否有新数据写入dirty,两者数据是否不一致
}
entry的数据结构 里面是一个指针,指向用户存储的value值
type entry struct{
p unsafe.Pointer
}
虽然read 和 dirty 有冗余数据,但这readOnly.m和Map.dirty存储的值类型是*entry,是一个指针,指向同一个数据,所以尽管map的value很大,冗余的空间占用还是有限的(疑问:直接指向value的指针不就可以了,为什么还要结构体再包一层,里面套个指针)
后续:解答上面的为什么包一层,是因为需要read和dirty两者指向同一个地方,这个地方里面的信息在一方修改状态的时候,另一方有感知,假设这里不包一层,read 和dirty分别直接指向value的地址,如果删除的时候,read直接把key对应的p改成nil,说明删除了,但是dirty里面影响不了,还是指向value的地址,后续如果提升dirty到read就会有问题,这个key正常应该是nil删除了,但是dirty赋值给read之后又’活过来‘了,这样显然有问题,所以如果包一层,两个map一直都在监听这个位置,如果read把里面的p置为nil,因为dirty的entry地址和read一样(实际read是从dirty复制来的),所以dirty也感知key删除了,即使提升到read也是给的entry的值,里面的信息和read是一致的,不会有问题
声明变量之后,往里面存储数据,这一步store方法底层干了什么呢?
store方法的源码如下
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {//如果read里面有或者只是软删除,更新value值
return
}
m.mu.Lock()//到这里有两种情况 1read里面没有 2read里面有key但是硬删除了
read, _ = m.read.Load().(readOnly)//double check
if e, ok := read.m[key]; ok {//第2种情况 read有但已经硬删除
if e.unexpungeLocked() {//这里把硬删除改为nil的软删除
m.dirty[key] = e//dirty重新增加这个key
}
e.storeLocked(&value)//更新e里面的指针指向value的地址
} else if e, ok := m.dirty[key]; ok {//read里面没有值,dirty里面有值
e.storeLocked(&value)//直接更新value值
} else {//read和dirty里面都没有值
if !read.amended {//这种情况一般是missLocked执行之后,dirty为nil了
m.dirtyLocked()//把read里面没有删除的数据复制给dirty,清理了删除的数据
m.read.Store(readOnly{m: read.m, amended: true})//read要标记不一致
}
m.dirty[key] = newEntry(value)//把新的key value放到dirty里面
}
m.mu.Unlock()
}
总结一下就是先看read,1.如果read有或者软删除就直接更新相应entry里的值,指向新的value的地址2.否则就加锁继续往下,如果read有硬删除,就修改为软删除标记,dirty增加这个key,read和dirty的entry都重新指向新值的地址3.如果read没有,dirty有,更新相应entry对应的值;4.如果都没有,直接在dirty新增,大致这样,当然还有一些细节
Store可能会在某种情况下(初始化或者m.dirty刚被提升后)从m.read中复制数据,如果这个时候m.read中数据量非常大,可能会影响性能,这部分逻辑在dirtyLocked方法里面
storeLocked的源码,就是把value值放在entry结构体中
// The entry must be known not to be expunged.
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {//不是删除的元素
m.dirty[k] = e//复制给dirty
}
}
}
dirtyLocked这个方法很重,因为里面是会遍历整个map把read里面没有被删除的数据重新复制回dirty里面,走到这个方法的条件有两个 1.missLocked刚执行完,把dirty置为nil了&&2.又来插入一个新的键值对 这两个条件容易出现在写多读少的场景,比如1一般是由大量的插入在dirty里面,read读不到才会miss到阈值,所以说sync.Map不适合写多读少的场景,这个方法的主要作用还有就是清除删除的数据,read里面一些软删除的数据就变为硬删除了,dirty里面不会存放了
如果read确实有并且没有删除,更新read里面key对应的value
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
存储之后通过load方法去检索是否有值
load 源码如下
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {//如果read里面没有,并且dirty和read数据不一致,可能有
m.mu.Lock()//加锁,去dirty里面寻找
read, _ = m.read.Load().(readOnly)//双检查,再次看read里面有没有,因为加锁期间可能read数据有变化
e, ok = read.m[key]
if !ok && read.amended {//read没有,dirty可能有
e, ok = m.dirty[key]
m.missLocked()//这是一个额外操作,和取值无关,因为read miss了一次,所以这个方法miss++ ,到一定程度,迁移dirty数据到read上,具体看下面方法详解
}
m.mu.Unlock()//取完解锁
}
if !ok {//有几种情况 1read没取到,dirty没有数据,只查看了read 2read没取到,dirty数据不一致(amended为true)查了dirty也没取到
return nil, false
}
return e.load()//取到,还要看具体value的情况返回,具体看下面发放解释
}
missLocked的源码如下,主要是miss如果过多了,说明每次都是先找read找不到再去dirty寻找,这样找两次,而且还加锁,效率很低,所以需要把dirty的数据给read,这样每次可以直接read找到返回
func (m *Map) missLocked() {
m.misses++//去dirty找一次,miss加1
if m.misses < len(m.dirty) {//miss的个数还没有达到dirty中元素的个数,直接返回
return
}
m.read.Store(readOnly{m: m.dirty})//miss已经过多,read存储dirty中的数据
m.dirty = nil//dirty清空 这里清空是一个必须操作吗为啥
m.misses = 0//miss清空
}
*entry 的load方法源码如下
func (e *entry) load() (value interface{}, ok bool) {
p := atomic.LoadPointer(&e.p)//取出指针
if p == nil || p == expunged {//虽然可以找到key对应的value的地址,但是地址内的数据已经删除了
return nil, false//返回false表示没有找到
}
return *(*interface{})(p), true//返回找到并且返回具体值
}
当想要删除某个key的时候
delete 的源码
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {//如果read没有但是dirty可能有
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {//双检查 如果read还是没有 dirty可能有
delete(m.dirty, key)//删除dirty里面的值,这个应该是硬删除,从这里可以看出如果删除一个不存在的值也不会报错,直接返回的,因为已经加锁了,所以这里可以直接删除掉key,不用考虑临界情况
}
m.mu.Unlock()
}
if ok {//read里面有值
e.delete()//软删除,将这个值对应的entry里面的p更新为nil
}
}
func (e *entry) delete() (value interface{}, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}
还有一个迭代功能
range遍历源码如下
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
if read.amended {//如果dirty有新数据
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {//双检查
read = readOnly{m: m.dirty}//把dirty 的值给read,这里amended默认是false,两者数据一致
m.read.Store(read)
m.dirty = nil//清空dirty
m.misses = 0
}
m.mu.Unlock()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
sync.Map没有Len方法,并且目前没有迹象要加上,所以如果想得到当前Map中有效的entries的数量,需要使用Range方法遍历一次
概括
sync.Map的原理很简单,使用了空间换时间策略,通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
通过引入两个map将读写分离到不同的map,其中read map提供并发读和已存元素原子写,而dirty map则负责读写。
这样read map就可以在不加锁的情况下进行并发读取,当read map中没有读取到值时,再加锁进行后续读取,并累加未命中数。
当未命中数大于等于dirty map长度,将dirty map上升为read map。
从结构体的定义可以发现,虽然引入了两个map,但是底层数据存储的是指针,指向的是同一份值。
思考
1.比普通的map好在哪里
举例 如果对map同时进行读a写b的操作
普通map因为不能并发读取,所以即使是对不同元素进行读写操作,也要在同一个队列里面。通过加锁来实现读写顺序执行
sync.map 优化点在于这是两个不相干的行为,通过底层两个map分离开同时进行,两个同时请求的时候,a在read里面读到直接返回,写b在read里面读不到,(同时都是读操作,这里不会阻塞)就去dirty里面写,即使加锁也不影响a
2.假设一个map一直只写,那么逻辑是 read里面没有,dirty里面也没有,最后加锁写在dirty里面read里面没有东西,和普通map差不多
3.假设一半读一半写,读read没有就去dirty读,miss次数达到条件,就是把dirty提升到read里面,这样读都从read里面取,完全不会被锁阻塞(这里凸显比普通map优秀的一点),除非读的是新写进dirty的值,这些还是和普通map一样需要进入读写加锁阻塞的流程,但是miss达到阈值,提升到read,这些元素又可以读写操作锁free了,如此反复
记住
1.只要涉及dirty,就和普通map一样要读写加锁了
2.只要上升到read,读取和更新值 都实现了锁free \
软删除的好处是重新插入的时候不需要加锁,代价比较小,数据在dirty里面还是存在的更新一下指向的value地址就可以,硬删除是需要加锁的
可以看出软删除可以通过无锁化的cas来完成更新,CAS就是compareandswap,而expunged硬删除dirty已经没有这个key了,不具有指向该entry的能力了
底层有闭环的两个map相互复制的情况,dirty在读miss达到阈值的时候会复制给read,dirty置nil,这个在missLocked方法里面
当插入新数据,dirty为nil时,需要遍历read把不删除的数据复制给dirty,并且软删除设置为硬删除,新数据插入到dirty中,这一步有一个作用就是清除那些删除的数据内存
这个视频讲的很透彻 www.bilibili.com/video/BV1uk…