Go进阶之sync.Map

34 阅读5分钟

sync.Map是一种并发安全的map.它位于标准库的sync包中.

1.用法:

1.1声明:

sync.Map不需要像原生map一样使用make或字面量初始化.

示例:

func main() {
    var m sync.Map
}

sync.Map的零值为空的map(并非nil).声明后可直接使用.

1.2增删改查:

func main() {
    var m sync.Map
    //增加.
    m.Store("a", 1)
    m.Store("b", 2)
    //修改
    m.Store("a", 3)
    //查询
    value, ok := m.Load("a")
    if ok {
       fmt.Println(value)
    }
    //删除
    m.Delete("b")
}

sync.Map无法使用方括号([])来指定键值.因为它由标准库提供.所有接口均有方法

提供.sync.Map可以存储任何类型的键值对.取出的元素类型为any(或interface{}).

使用时需要使用类型断言.

1.3其他接口:

LoadOrStore:

可以理解为避免覆盖的store.如果指定的键存在.那么LoadStore将由actual返回相

应的值.并将loaded置为true.表示返回值是已存在的.如果知道的键不存在.那么ac

tual返回插入的值.并将loaded值设置为false.表示返回值并非存在.是插入的.

LoadAndDelete:

再删除一个键时.如果指定的键存在.能够返回被删除的键值.如果指定的键不存

在.loaded值将为false表示键不存在.

Range:

sync.Map不能像原生map那样使用range进行遍历.所以提供了Range方法实现遍

历能力.Range会遍历每一个键值对并逐个调用回调函数.

注:sync.Map支持并发读写.遍历期间可能读到其他协程写入的数据.返回的键值可能

和遍历开始时的map数据不一致.

2.使用要点:

1).特定场景下可提升性能.

内部实现采用了两个原生map实现读写分离.数据读取并且能命中时能够提升性能.否

则性能可能不如原生.只有在读多写少的场景下性能才有优势.并非所有场景.

因为sync.Map使用了两个map.不可避免的会用更多地内存.对GC带来压力.系统对

内存大小和GC敏感的场景下尽量避免使用.

2).警惕类型安全风险.

func main() {
    var m sync.Map
    m.Store("a", 1)
    m.Store(1, "a")
    m.Range(func(k, v any) bool {
       //panic.
       fmt.Println(k.(string), v.(int))
       return true
    })
}

sync.Map不能像原生map那样声明键和值的类型.所以需要进行断言.来防止出发

panic.

3).不能拷贝和传递.

sync.Map内部实现中使用了锁.因为锁是不能拷贝的.所以无法拷贝.

2.实现原理:

2.1数据结构:

type Map struct {
    _ noCopy

    mu Mutex

    read atomic.Pointer[readOnly]

    dirty map[any]*entry

    misses int
}

readOnly结构如下:

type readOnly struct {
    m       map[any]*entry
    amended bool // true if the dirty map contains some key not in m.
}

由此可见.sync.Map是由两个map表组成.read表提供并发读能力.新数据则写入

dirty表中.dirty表仅仅是新数据的存放区.数据最终会同步到read表.同步的时机则

取决于misses.读数据先查找read表.如果未找到则记录一次miss.待到miss次数足

够多(miss数同于数据总数)时.则会触发数据同步.

sync.Map中的互斥锁主要用于保护dirty表.

readOnly结构中的amended变量用于标记dirty表中是否有额外数据.当有额外数

据插入.标记位true.当数据同步后.重置为false.

2.1.1entry数据结构:

type entry struct {
  
    p atomic.Pointer[any]
}

entry是map中存放数据的槽位.使用entry类型的指针可以让read表和dirty表实现

数据共享.从而避免内存浪费.

3增删改查:

3.1插入数据:

插入的数据首先会被放置在dirty表中.

如图所示三条数据存入了dirty表中.需要注意的是read所指向的readOnly对象中的

amended已经变成了true.表示了dirtcy表中暂存了部分未同步的数据.

3.2查找数据:

承接上图.查找数据的时候,会先查询read表.如果查到了直接返回.如果数据不存在.则

会根据amended来决定是否要继续查询dirty表.当amended为true时会尝试继续

查询.如果为false则不会继续.

当查询需要查询dirty表时.则read表会记录一次miss.当miss次数等于dirty表的数

据时.则会把dirty数据全部同步过来.

3.3再次插入:

再次插入数据分为两种情况.

数据已存在.相当于修改数据.此时会从表中取出相应的值并使用原子操作直接完成修改.

数据不存在.则数据会像前面第一次插入数据一样写入dirty表.但此次向dirty表中写

入数据时会把read表中的全部数据复制过来.

注:dirty表冗余read表中的数据是为了后续同步的时候可以用整表替换.不用挨个复

制.删除时只会清除entry中的数据.而entry表中的槽位将会保留下来.当dirty表复制的时候会把这些槽位清除掉.从而实现垃圾回收的效果.

3.4删除数据:

承接上图.当删除数据时.会根据数据存在的位置分为两种情况处理.

如果删除的数据存在于read表中.直接把相应的entry值置为nil.

如果删除的数据存在于dirty表中.则直接从dirty表中删除整个键值对.

由于原生map不支持并发读写.为了避免读写冲突.当从read表中删除数据时不能直

接删除键值对.所以只是把entry置为nil.空的槽位会在下次dirty表冗余的时候清除

掉.

走过了这条街.人间烟火.



如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路