Go中并发安全的字典sync.Map
文章目录
一、sync.Map的特点
sync.Map 这个字典类型提供了一些常用的键值存取操作方法,并保证了这些操作的并发安全。同时,它的存、取、删等操作都可以保证在常数时间内执行完毕。它们的算法复杂度与map类型一样都是O(1)的。
sync.Map 本身虽然用到了锁,但是它其实在尽可能地避免使用锁。
二、使用sync.Map 的注意事项
2.1 并发安全字典对键的类型的要求
必须保证键的类型是可比较的, 键的实际类型不能是:函数类型、字典类型、切片。
因为并发字典内部使用的存储介质正是原生字典,Go语言的原生字典的键类型不能是函数类型、字典类型、切片类型。
我们应该在每次操作并发安全字典的时候,都去显式检查键值的实际类型。无论是存、取、删都应该如此。
可以通过调用reflect.TypeOf
函数得到一个键值对应的反射类型值(即:reflect.Type 类型的值),然后再调用这个值的Comparable 方法,得到确切的判断结果。
2.2 保证并发安全字典中的键和值的类型正确性
使用断言表达式或反射操作来保证它们的类型正确性。
(1)方案一:完全确定键和值具体类型的情况
让并发安全字典只能存储某个特定类型的键。
一旦确定了键的类型,就可以使用类型断言表达式去对键的类型做检查了。并且,如果把并发安全字典封装在一个结构体类型里面,那就更加方便了。这时候可以让Go语言编译器帮助做类型检查
package main
import "sync"
type IntStrMap struct {
m sync.Map
}
func (iMap *IntStrMap) Delete(key int) {
iMap.m.Delete(key)
}
func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
v, ok := iMap.m.Load(key)
if v != nil {
value = v.(string)
}
return
}
func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
a, loaded := iMap.m.LoadOrStore(key, value)
actual = a.(string)
return
}
func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
f1 := func(key, value interface{
}) bool {
return f(key.(int), value.(string))
}
iMap.m.Range(f1)
}
func (iMap *IntStrMap) Store(key int, value string) {
iMap.m.Store(key, value)
}
(2)方案二:初始化并发安全字典时,动态地给定键和值的类型
设计结构体类型,除了包含sync.Map
字段,还要包含两个反射类型reflect.Type
,用于保存键类型和值类型:
type ConcurrentMap struct {
m sync.Map
keyType reflect.Type
valueType reflect.Type
}
reflect.Type
这个类型可以代表Go语言中的任何数据类型,并且,这个类型的值非常容易获得:通过调用reflect.TypeOf 函数把某个样本值传入即可。
func main() {
v1 := int(32)
res := reflect.TypeOf(v1)
// 打印int
fmt.Println(res)
}
由于反射类型值之间可以直接使用操作符==或!=进行判等,所以这里等类型检查代码非常简单。
func (cMap *ConcurrentMap) Load(key interface{
}) (value interface{
}, ok bool) {
if reflect.TypeOf(key) != cMap.keyType {
return
}
return cMap.m.Load(key)
}
再看Store
函数:
func (cMap *ConcurrentMap) Store(key, value interface{
}) {
if reflect.TypeOf(key) != cMap.keyType {
panic(fmt.Errorf("错误的key类型:%v", reflect.TypeOf(key)))
}
if reflect.TypeOf(value) != cMap.valueType {
panic(fmt.Errorf("错误的value类型:%v", reflect.TypeOf(key)))
}
cMap.Store(key, value)
}
(3)两种方案等优缺点
第一种方案:
- 适用于我们完全确定键和值具体类型的情况,可以利用Go语言编译器去做类型检查,并使用类型断言做辅助,就像上面
IntStrMap
一样。 - 但是这种方案无法灵活地改变字典的键和值类型。一旦需求多样化,编码的工作量也随之而来。
第二种方案:
- 无需再程序运行之前就明确键和值的类型,只要在初始化并发安全字典的时候,动态的给定它们。这里主要使用reflect包中的函数和数据类型,另加一些简单的判等操作;
- 这些反射操作会或多或少会降低程序的性能。
三、并发安全字典如何做到尽量避免使用锁
sync.Map类型在内部使用了大量的原子操作来存取键和值,并使用了两个原生的map作为存储介质。
-
其中一个原生map被存在了sync.Map的read字段中,该字段时sync/automic.Value 类型的。
它会在条件满足的时,去重新保存所属的sync.Map 中包含的所有键值对。
它先把值转换为了unsafe.Pointer类型的值,然后再把后者封装,并储存在其中的原生字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子操作了。
-
sync.Map中的另一个原生字典由它的dirty字段代表。在它上面操作需要用到锁。
不需要用到锁的场景:
-
sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。
只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。
-
相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jg1KDn9j-1676813284143)(./photos/2023-02-19-read和dirty两个原生字典.webp)]