并发安全的sync.Map

对于并发读写map的情况下,map的数据会被写乱造成panic。Go语言原生map并不是线程安全的,因此对它进行并发读写操作时需要加锁。但是当操作频繁且要求性能的情况下,锁的优化已经无法满足业务需求,考虑到互联网应用通常是读多写少的场景,Golang的标准库提供了一个特殊的并发安全的map实现,为了与原生map区分,人们习惯性称为sync.map。

 一、sync.map与map的区别

1.并发安全

sync.Map内部使用了锁和其他同步原语来保证并发访问的安全性

2.无需初始化

sync.Map的零值是为空的map不是nil,因此不需要使用make函数初始化,可以直接声明后使用。

3.特殊的API

sync.Map提供特定的方法如Load、Store等,与内置map的语法不同

二、基础增删改查,遍历及其他方法

增删改查+遍历
func text(){
    var mymap sync.Map


/*
增(或修改)
*/
    mymap.Store("apple",10)   //增加键为apple值为10的数据
    mymap.Store("apple",5 )    //修改键为apple的值为5



/*
查
*/

    num, _ := mymap.Load("apple")
    fmt.Printf("apple num=%d\n",num.(int))//输出5


/*
删
*/
    mymap.Delete("apple")//删除apple及对应的值


/*
遍历
*/
    mymap.Range(func(key,value interface{})bool{
        fmt.Printf("%s:%d\n",key,value)
        return true
    })

}
其他方法
(1)LoadOrStore
func main(){
    var mymap sync.Map
    sub1, ok1 := mymap.LoadOrStore("key1","value1")
    fmt.Println(sub1,ok1)//输出value1,false

    sub2,ok2 := mymap.LoadOrStore("key1","new_value1")
    fmt.Println(sub2,ok2)//输出 value1,true
}

通过示例,在sync.Map中使用LoadOrStore方法添加键值对时,若key存在,则返回旧的值和true;若key不存在,则会添加一个新的键值对并返回相应的值和false

(2)LoadAndDelete

若被删除的键存在,则返回被删除的键值和true;若不存在则返回false表示键不存在

三、sync.Map的适用场景

sync.Map通过使用read和dirty两个map来进行读写分离,降低锁时间来提高效率。

sync的使用场景

1.key的集合基本不变,但是value会并发更新:在这种场景下,sync.Map通过将热点数据分离出来,减少锁的争用,提高了性能。

2.k-v对的添加和删除操作比较少,但是读的操作非常频繁:sync.Map在读取操作山做了优化,读操作通常无需加锁,这大大提高了并发读的性能。

sync的适用场景

1.并发环境下的缓存系统:缓存项被频繁读取,但更新和删除操作比较少。

2.长运行的监听器列表:监听器被添加后很少改变,但可能会被频繁触发。

3.全局状态和配置:全局配置可能会在程序启动时被设置,之后只会被读取。

四、sync.Map设计原理及源码分析

1.核心思想

尽可能无锁化:要实现并发安全,很难做到无锁化。但是为了提高性能,应该尽可能使用原子操作,最大化减少锁的使用。

读写分离:读写分离式针对读多写少场景的常用手段,面对读多写少的场景能够提供高性能的访问。

2.数据结构分析

sync.Map的结构体定义

// sync/map.go
type Map struct {
    mu Mutex          // 互斥锁,用于保护dirty字段和misses字段。
    read atomic.Value // readOnly, 一个atomic.Value类型的字段,存储了一个readOnly结构体,用于存储只读数据。
    dirty map[interface{}]*entry // 一个map,存储了所有可写的键值对。
    misses int    // 一个计数器,记录了从read读取失败的次数,用于触发将数据从dirty迁移到read的决策。
}

type readOnly struct {
    m       map[interface{}]*entry    // 实际存储键值对的map。
    amended bool // 标记位,如果dirty中有read中没有的键,那么为true
}
sync.Map使用两个原生的map(本质上是map[interface{}]*entry)来作为数据的存储空间分别是:
  • read:只读字典, 使用atomic.Value来承载,保证原子性和高性能, 但不保证数据的完整性(不保证拥有全部的Key),相当于某个时间的Key-Value对的快照。但如果需要更新 read,则需要加锁保护。对于 read 中存储的 entry 字段,可能会被并发地 CAS 更新。但是如果要更新一个之前已被删除的 entry,则需要先将其状态从 expunged 改为 nil,再拷贝到 dirty 中,然后再更新。
  • dirty:脏字典, 用互斥锁Map.mu来保护,保证了并发安全。如果m.dirty!=nil, 则dirty包含了所有的Key-Value对。当新增一个Key时,会先存放在dirty中,然后等满足一定条件后再同步给read。如果dirty为 nil,那么下一次写入时,会新建一个新的dirty,这个初始的dirty是read 的一个拷贝,但除掉了其中已被删除的 key。

 entry是对实际数据的封装

type entry struct{
    p unsafe.Pointer  //*interface{}  一个指向实际数据的指针
}
var EXPUNGED = unsafe.Pointer(new(any))
entry中的p有三种情况:

1.e.p==nil:

entry已经被标记删除,不过因为还未进行read=>dirty的同步,因此dirty中可能还存在该entry

2.e.p == expunged

entry已经被标记删除,且已经完成read=>dirty同步,因而不属于dirty,仅仅属于read,下一次dirty=>read升级,会被彻底清理。延迟删除的思想。

3.e.p为正常值

键值对存在,存在于m.read.m中,如果m.dirty!=nil 则存在于m.dirty

总结

1.sync.Map是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。

2.通过读写分离,降低锁时间来提高效率,适用于读多写少的场景。

3.Range 操作需要提供一个函数,参数 是 k,v ,返回值是一个布尔值:f func(key,value interface{}) bool

4.调用 Load 或 LoadOrStore 函数时,如果在 read 中没有找到 key,则会将 misses 值原子地增加 1,当 misses 增加到和 dirty 的长度相等时,会将 dirty 提升为 read。以期减少“读 miss”。

5.新写入的 key 会保存到 dirty 中,如果这时 dirty 为 nil,就会先新创建一个 dirty,并将 read 中未被删除的元素拷贝到 dirty。

6.当 dirty 为 nil 的时候,read 就代表 map 所有的数据;当 dirty 不为 nil 的时候,dirty 才代表 map 所有的数据。

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值