【Go专家编程——常见数据结构的实现原理sync.Map】

1.sync.Map

sync.Map是一种并发安全的map,相较于原生map。sync.Map在并发读写时不会触发panic,它可以减轻程序员的负担。不用再小心翼翼地处理各种锁。

2.基础知识

  • sync.Map在读多写少的场景下的性能优于原生map+锁的机制。
  • sync.Map采用了两个冗余的数据结构结构(read map和dirty map)来实现读写分离。
  • sync.Map使用了锁,所以不能被拷贝
  • sync.Map在查询已存在的元素时,往往能够做到无锁访问。
  • sync.Map是针对特定场景的优化,并不能用于完全取代原生map。

3. 特性速览

3.1 用法

声明
sync.Map不需要使用make或字面量进行初始化
var m sync.Map,也就是说声明之后即可直接使用

增删改查

//增加(修改)
m.Store("Jim",80)//写入
m.Store("Kevin",85)
m.Store("Jim",90)//修改
//查询
score,_ := m.Load("Jim")
//删除
m.Delete("Jim")

sync.Map无法使用方括号[]来指定键值,所有接口均由方法提供。
sync.Map可存储任何类型的键值对,取出的元素类型为any(或interface{}),使用时需要使用类型断言。

其它接口

  • LoadOrStore(key,value any) (actual any, loaded bool)
    • 避免覆盖的Store
    • 如果指出的键存在,那么LoadOrStore将由actual返回相应的值。
    • 如果指出的键不存在,那么将这个键值对存入map
  • LoadAndDelete(key any) (value any, loaded bool)
    • 删除一个键,如果指定的键存在,则返回被删除的键值(类似于pop)
  • Range(f func(key, value any) bool)
    • sync.Map不能像原生map那样使用range遍历,提供了Range方法来实现遍历。Range会遍历每一个键值对并逐个调用回调函数,借此实现遍历。

3.2 使用要点

3.2.1 特定场景下可提升性能

sync.Map的内部实现采用了两个原生map来实现读写分离,数据读取并且能命中时能够提升性能,否则性能可能不如原生map,它仅在读多写少的场景下性能才有优势,并非在任意场景下的性能都优于原生map。

由于sync.Map会使用两个冗余的原生map,会使用更多的内存。无形中增加了GC的压力,在对内存大小或GC敏感的场景下应尽可能避免使用sync.Map

3.2.2 警惕类型安全风险

声明sync.Map时并不像声明原生map时那样指定了键和值的类型,事实上可以存储任意类型的数据,我们需要对返回的值进行类型断言,否者会在类型转换上触发panic风险。

3.2.3 不能拷贝和传递

由于结构中使用了锁sync.Mutex,因为锁是不能拷贝的(否则会造成死锁或触发panic)。所以sync.Map也不能拷贝

4. 实现原理

4.1 数据结构

4.1.1 sync.Map的数据结构

Go标准库中定义了sync.Map的数据结构

type Map struct{
	mu Mutex
	read atomic.Value	//read表,允许并发读
	dirty map[any]*entry	//dirty表,负载新数据写入
	misses int	// 记录read表查找miss的次数
}

dirty表仅仅是新数据的临时存放区,数据最终会同步会read表,同步的时机取决于misses,读取数据时会先查找read表,如果未找到则记录一次miss,待miss次数足够多(miss数等同于数据总数)时,则会触发数据同步。

sync.Map中高度互斥锁mu主要用于保护dirty表,同时在数据由dirty向read同步时起保护作用,避免多个同步操作并发执行。

4.1.2 readOnly的数据结构

前面看到sync.Map数据结构中read表的类型为atomic.Value,实际保存的数据则是名为readOnly的结构体

type readOnly struct{
	m	map[any]*entry
	// 标记dirty表中是否有不存在于read表中的数据
	amended bool
}
  • 当有新数据插入dirty时,amended标记为true。
  • 此时查询数据,若read表中不存在则会继续查询dirty表。
  • 当dirty表中的数据同步到read之后,amended为false
  • 此时查询数据,若read表中不存在则直接结束,省去加锁并查询dirty的时间花销
  • read表使用原子类型,主要是为了在数据同步(针对read的写操作)时不必阻塞其读取操作

4.1.3 entry的数据结构

entry是map中存放数据的曹巍,使用的是指针类型,好处是read表和dirty表可以实现共享内存,从而避免内存浪费。

type entry struct{
	p unsafe.Pointer	// *interface{}
}

4.2 增删改查

4.2.1 插入数据

流程:

  • 将数据插入dirty表
  • 将read表中的amended置为true,表面dirty表中有read表没有的数据

4.2.2 查找数据

流程:

  • 先读read表,若查找不到
  • 检查amended标志,为false则结束
  • 为true则查找dirty表,misses+1.当misses次数等于dirty表的大小事,触发转移

4.2.3 再次插入数据

流程:

  • 如果已存在
    • 则从read表中取出对应的值并使用原子操作直接完成修改
  • 若不存在
    • 则向dirty表中写数据,(同时把read表中的数据全部”复制过来“)

为什么需要把read表中的数据复制过来?

  • dirty表通过冗余read表中的数据从而维护一个全量数据,等到数据同步时,可以采用整表替换,不需要逐个遍历。
  • 同时read表中的数据可能会被删除,会存在一些空的entry槽位
  • 在这个”复制的过程“中其实也在剔除这些空的entry槽位,达到垃圾回收的效果
  • 所以当写多的场景时效率会低下

4.2.4 删除数据

流程:

  • 如果要删除的数据存在于read表中
    • 则直接把对应的entry的值置为nil
  • 如果数据仅存在于dirty表中
    • 则直接从dirty表中删除整个键值对,
  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
sync.MapGo 语言标准库中提供的一种并发安全的字典类型,它可以被多个 goroutine 安全地访问和修改。在多个 goroutine 中并发地读写一个 map 时,会出现竞争条件,从而导致数据不一致。而 sync.Map 利用了一些锁的技巧,避免了这种竞争条件的发生,从而实现了高效的并发安全访问。 sync.Map 的 API 非常简单,主要包括以下几个方法: 1. Store(key, value interface{}):将一个键值对存储到 sync.Map 中。 2. Load(key interface{}) (value interface{}, ok bool):根据键从 sync.Map 中获取对应的值。 3. LoadOrStore(key, value interface{}) (actual interface{}, loaded bool):如果键存在于 sync.Map 中,则返回对应的值和 true,否则将键值对存储到 sync.Map 中并返回新的值和 false。 4. Delete(key interface{}):从 sync.Map 中删除一个键值对。 5. Range(f func(key, value interface{}) bool):遍历 sync.Map 中的键值对,并对每一个键值对调用函数 f,如果 f 返回 false,则停止遍历。 下面是一个使用 sync.Map 的简单例子,展示了如何在多个 goroutine 中并发地访问和修改 sync.Map: ``` package main import ( "fmt" "sync" ) func main() { var m sync.Map var wg sync.WaitGroup wg.Add(2) // goroutine 1: 向 sync.Map 中存储键值对 go func() { defer wg.Done() m.Store("key1", "value1") m.Store("key2", "value2") }() // goroutine 2: 从 sync.Map 中加载键值对 go func() { defer wg.Done() if v, ok := m.Load("key1"); ok { fmt.Println("value for key1:", v) } if v, ok := m.Load("key2"); ok { fmt.Println("value for key2:", v) } }() wg.Wait() } ``` 在上面的例子中,我们首先创建了一个 sync.Map 对象 m。然后在两个 goroutine 中同时访问这个对象,一个 goroutine 向其中存储键值对,另一个 goroutine 则从其中加载键值对。由于 sync.Map 是并发安全的,所以这两个 goroutine 可以并发地访问和修改 sync.Map,而不会出现竞争条件。 需要注意的是,虽然 sync.Map 是并发安全的,但它并不是用来替代普通的 map 的。如果你只是需要在某个 goroutine 中访问和修改一个 map,那么你应该使用普通的 map,因为 sync.Map 的性能会比较差。只有在需要多个 goroutine 并发地访问和修改一个 map 时,才应该考虑使用 sync.Map
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值