Go中并发安全的字典sync.Map

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)]

猜你喜欢

转载自blog.csdn.net/hefrankeleyn/article/details/129115110
今日推荐