35 | 并发安全字典sync.Map (下)

我们在上一篇文章中谈到了,由于并发安全字典提供的方法涉及的键和值的类型都是interface{},所以我们在调用这些方法的时候,往往还需要对键和值的实际类型进行检查。

这里大致有两个方案。我们上一篇文章中提到了第一种方案,在编码时就完全确定键和值的类型,然后利用 Go 语言的编译器帮我们做检查。

这样做很方便,不是吗?不过,虽然方便,但是却让这样的字典类型缺少了一些灵活性。

如果我们还需要一个键类型为uint32并发安全字典的话,那就不得不再如法炮制地写一遍代码了。因此,在需求多样化之后,工作量反而更大,甚至会产生很多雷同的代码。

知识扩展

问题 1:怎样保证并发安全字典中的键和值的类型正确性?(方案二)

那么,如果我们既想保持sync.Map类型原有的灵活性,又想约束键和值的类型,那么应该怎样做呢?这就涉及了第二个方案。

在第二种方案中,我们封装的结构体类型的所有方法,都可以与sync.Map类型的方法完全一致(包括方法名称和方法签名)。

不过,在这些方法中,我们就需要添加一些做类型检查的代码了。另外,这样并发安全字典的键类型和值类型,必须在初始化的时候就完全确定。并且,这种情况下,我们必须先要保证键的类型是可比较的。

所以在设计这样的结构体类型的时候,只包含sync.Map类型的字段就不够了。

比如:

type ConcurrentMap struct {m         sync.MapkeyType   reflect.TypevalueType reflect.Type
}

这里ConcurrentMap类型代表的是:可自定义键类型和值类型的并发安全字典。这个类型同样有一个sync.Map类型的字段m,代表着其内部使用的并发安全字典。

另外,它的字段keyType和valueType,分别用于保存键类型和值类型。这两个字段的类型都是reflect.Type,我们可称之为反射类型。

这个类型可以代表 Go 语言的任何数据类型。并且,这个类型的值也非常容易获得:通过调用reflect.TypeOf函数并把某个样本值传入即可。

调用表达式reflect.TypeOf(int(123))的结果值,就代表了int类型的反射类型值。

我们现在来看一看ConcurrentMap类型方法应该怎么写。

先说Load方法,这个方法接受一个interface{}类型的参数key,参数key代表了某个键的值。

因此,当我们根据 ConcurrentMap 在m字段的值中查找键值对的时候,就必须保证 ConcurrentMap 的类型是正确的。由于反射类型值之间可以直接使用操作符==或!=进行判等,所以这里的类型检查代码非常简单。

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {if reflect.TypeOf(key) != cMap.keyType {return}return cMap.m.Load(key)
}

我们把一个接口类型值传入reflect.TypeOf函数,就可以得到与这个值的实际类型对应的反射类型值。

因此,如果参数值的反射类型与keyType字段代表的反射类型不相等,那么我们就忽略后续操作,并直接返回。

这时,Load方法的第一个结果value的值为nil,而第二个结果ok的值为false。这完全符合Load方法原本的含义。

再来说Store方法。Store方法接受两个参数key和value,它们的类型也都是interface{}。因此,我们的类型检查应该针对它们来做。

func (cMap *ConcurrentMap) Store(key, value interface{}) {if reflect.TypeOf(key) != cMap.keyType {panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))}if reflect.TypeOf(value) != cMap.valueType {panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))}cMap.m.Store(key, value)
}

这里的类型检查代码与Load方法中的代码很类似,不同的是对检查结果的处理措施。当参数key或value的实际类型不符合要求时,Store方法会立即引发 panic。

这主要是由于Store方法没有结果声明,所以在参数值有问题的时候,它无法通过比较平和的方式告知调用方。不过,这也是符合Store方法的原本含义的。

如果你不想这么做,也是可以的,那么就需要为Store方法添加一个error类型的结果。

并且,在发现参数值类型不正确的时候,让它直接返回相应的error类型值,而不是引发 panic。要知道,这里展示的只一个参考实现,你可以根据实际的应用场景去做优化和改进。

至于与ConcurrentMap类型相关的其他方法和函数,我在这里就不展示了。它们在类型检查方式和处理流程上并没有特别之处。你可以在 demo72.go 文件中看到这些代码。

稍微总结一下。第一种方案适用于我们可以完全确定键和值具体类型的情况。在这种情况下,我们可以利用 Go 语言编译器去做类型检查,并用类型断言表达式作为辅助,就像IntStrMap那样。

在第二种方案中,我们无需在程序运行之前就明确键和值的类型,只要在初始化并发安全字典的时候,动态地给定它们就可以了。这里主要需要用到reflect包中的函数和数据类型,外加一些简单的判等操作。

第一种方案存在一个很明显的缺陷,那就是无法灵活地改变字典的键和值的类型。一旦需求出现多样化,编码的工作量就会随之而来。

第二种方案很好地弥补了这一缺陷,但是,那些反射操作或多或少都会降低程序的性能。我们往往需要根据实际的应用场景,通过严谨且一致的测试,来获得和比较程序的各项指标,并以此作为方案选择的重要依据之一。

问题 2:并发安全字典如何做到尽量避免使用锁?

sync.Map类型在内部使用了大量的原子操作来存取键和值,并使用了两个原生的map作为存储介质。

其中一个原生map被存在了sync.Map的read字段中,该字段是sync/atomic.Value类型的。 这个原生字典可以被看作一个快照,它总会在条件满足时,去重新保存所属的sync.Map值中包含的所有键值对。

为了描述方便,我们在后面简称它为只读字典。不过,只读字典虽然不会增减其中的键,但却允许变更其中的键所对应的值。所以,它并不是传统意义上的快照,它的只读特性只是对于其中键的集合而言的。

由read字段的类型可知,sync.Map在替换只读字典的时候根本用不着锁。另外,这个只读字典在存储键值对的时候,还在值之上封装了一层。

它先把值转换为了unsafe.Pointer类型的值,然后再把后者封装,并储存在其中的原生字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子操作了。

sync.Map中的另一个原生字典由它的dirty字段代表。 它存储键值对的方式与read字段中的原生字典一致,它的键类型也是interface{},并且同样是把值先做转换和封装后再进行储存的。我们暂且把它称为脏字典。

注意,脏字典和只读字典如果都存有同一个键值对,那么这里的两个键指的肯定是同一个基本值,对于两个值来说也是如此。

正如前文所述,这两个字典在存储键和值的时候都只会存入它们的某个指针,而不是基本值。

sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。

相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。

否则,它才会在锁的保护下把键值对存储到脏字典中。这个时候,该键值对的“已删除”标记会被抹去。

sync.Map 中的 read 与 dirty

顺便说一句,只有当一个键值对应该被删除,但却仍然存在于只读字典中的时候,才会被用标记为“已删除”的方式进行逻辑删除,而不会直接被物理删除。

这种情况会在重建脏字典以后的一段时间内出现。不过,过不了多久,它们就会被真正删除掉。在查找和遍历键值对的时候,已被逻辑删除的键值对永远会被无视。

对于删除键值对,sync.Map会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。

最后,sync.Map会把该键值对中指向值的那个指针置为nil,这是另一种逻辑删除的方式。

除此之外,还有一个细节需要注意,只读字典和脏字典之间是会互相转换的。在脏字典中查找键值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read字段中,然后把代表脏字典的dirty字段的值置为nil。

在这之后,一旦再有新的键值对存入,它就会依据只读字典去重建脏字典。这个时候,它会把只读字典中已被逻辑删除的键值对过滤掉。理所当然,这些转换操作肯定都需要在锁的保护下进行。

sync.Map 中 read 与 dirty 的互换

综上所述,sync.Map的只读字典和脏字典中的键值对集合,并不是实时同步的,它们在某些时间段内可能会有不同。

由于只读字典中键的集合不能被改变,所以其中的键值对有时候可能是不全的。相反,脏字典中的键值对集合总是完全的,并且其中不会包含已被逻辑删除的键值对。

因此,可以看出,在读操作有很多但写操作却很少的情况下,并发安全字典的性能往往会更好。在几个写操作当中,新增键值对的操作对并发安全字典的性能影响是最大的,其次是删除操作,最后才是修改操作。

如果被操作的键值对已经存在于sync.Map的只读字典中,并且没有被逻辑删除,那么修改它并不会使用到锁,对其性能的影响就会很小。

总结

这两篇文章中,我们讨论了sync.Map类型,并谈到了怎样保证并发安全字典中的键和值的类型正确性。

为了进一步明确并发安全字典中键值的实际类型,这里大致有两种方案可选。

  • 其中一种方案是,在编码时就完全确定键和值的类型,然后利用 Go 语言的编译器帮我们做检查。
  • 另一种方案是,接受动态的类型设置,并在程序运行的时候通过反射操作进行检查。

这两种方案各有利弊,前一种方案在扩展性方面有所欠缺,而后一种方案通常会影响到程序的性能。在实际使用的时候,我们一般都需要通过客观的测试来帮助决策。

另外,在有些时候,与单纯使用原生字典和互斥锁的方案相比,使用sync.Map可以显著地减少锁的争用。sync.Map本身确实也用到了锁,但是,它会尽可能地避免使用锁。

可能地避免使用锁。这就要说到sync.Map对其持有两个原生字典的巧妙使用了。这两个原生字典一个被称为只读字典,另一个被称为脏字典。通过对它们的分析,我们知道了并发安全字典的适用场景,以及每种操作对其性能的影响程度。

思考题

今天的思考题是:关于保证并发安全字典中的键和值的类型正确性,你还能想到其他的方案吗?

package mainimport ("errors""fmt""reflect""sync"
)// IntStrMap 代表键类型为int、值类型为string的并发安全字典。
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)
}// ConcurrentMap 代表可自定义键类型和值类型的并发安全字典。
type ConcurrentMap struct {m         sync.MapkeyType   reflect.TypevalueType reflect.Type
}func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {if keyType == nil {return nil, errors.New("nil key type")}if !keyType.Comparable() {return nil, fmt.Errorf("incomparable key type: %s", keyType)}if valueType == nil {return nil, errors.New("nil value type")}cMap := &ConcurrentMap{keyType:   keyType,valueType: valueType,}return cMap, nil
}func (cMap *ConcurrentMap) Delete(key interface{}) {if reflect.TypeOf(key) != cMap.keyType {return}cMap.m.Delete(key)
}func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {if reflect.TypeOf(key) != cMap.keyType {return}return cMap.m.Load(key)
}func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {if reflect.TypeOf(key) != cMap.keyType {panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))}if reflect.TypeOf(value) != cMap.valueType {panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))}actual, loaded = cMap.m.LoadOrStore(key, value)return
}func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {cMap.m.Range(f)
}func (cMap *ConcurrentMap) Store(key, value interface{}) {if reflect.TypeOf(key) != cMap.keyType {panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))}if reflect.TypeOf(value) != cMap.valueType {panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))}cMap.m.Store(key, value)
}// pairs 代表测试用的键值对列表。
var pairs = []struct {k intv string
}{{k: 1, v: "a"},{k: 2, v: "b"},{k: 3, v: "c"},{k: 4, v: "d"},
}func main() {// 示例1。var sMap sync.Map//sMap.Store([]int{1, 2, 3}, 4) // 这行代码会引发panic。_ = sMap// 示例2。{var iMap IntStrMapiMap.Store(pairs[0].k, pairs[0].v)iMap.Store(pairs[1].k, pairs[1].v)iMap.Store(pairs[2].k, pairs[2].v)fmt.Println("[Three pairs have been stored in the IntStrMap instance]")iMap.Range(func(key int, value string) bool {fmt.Printf("The result of an iteration in Range: %d, %s\n",key, value)return true})k0 := pairs[0].kv0, ok := iMap.Load(k0)fmt.Printf("The result of Load: %v, %v (key: %v)\n",v0, ok, k0)k3 := pairs[3].kv3, ok := iMap.Load(k3)fmt.Printf("The result of Load: %v, %v (key: %v)\n",v3, ok, k3)k2, v2 := pairs[2].k, pairs[2].vactual2, loaded2 := iMap.LoadOrStore(k2, v2)fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",actual2, loaded2, k2, v2)v3 = pairs[3].vactual3, loaded3 := iMap.LoadOrStore(k3, v3)fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",actual3, loaded3, k3, v3)k1 := pairs[1].kiMap.Delete(k1)fmt.Printf("[The pair with the key of %v has been removed from the IntStrMap instance]\n",k1)v1, ok := iMap.Load(k1)fmt.Printf("The result of Load: %v, %v (key: %v)\n",v1, ok, k1)v1 = pairs[1].vactual1, loaded1 := iMap.LoadOrStore(k1, v1)fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",actual1, loaded1, k1, v1)iMap.Range(func(key int, value string) bool {fmt.Printf("The result of an iteration in Range: %d, %s\n",key, value)return true})}fmt.Println()// 示例2。{cMap, err := NewConcurrentMap(reflect.TypeOf(pairs[0].k), reflect.TypeOf(pairs[0].v))if err != nil {fmt.Printf("fatal error: %s", err)return}cMap.Store(pairs[0].k, pairs[0].v)cMap.Store(pairs[1].k, pairs[1].v)cMap.Store(pairs[2].k, pairs[2].v)fmt.Println("[Three pairs have been stored in the ConcurrentMap instance]")cMap.Range(func(key, value interface{}) bool {fmt.Printf("The result of an iteration in Range: %d, %s\n",key, value)return true})k0 := pairs[0].kv0, ok := cMap.Load(k0)fmt.Printf("The result of Load: %v, %v (key: %v)\n",v0, ok, k0)k3 := pairs[3].kv3, ok := cMap.Load(k3)fmt.Printf("The result of Load: %v, %v (key: %v)\n",v3, ok, k3)k2, v2 := pairs[2].k, pairs[2].vactual2, loaded2 := cMap.LoadOrStore(k2, v2)fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",actual2, loaded2, k2, v2)v3 = pairs[3].vactual3, loaded3 := cMap.LoadOrStore(k3, v3)fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",actual3, loaded3, k3, v3)k1 := pairs[1].kcMap.Delete(k1)fmt.Printf("[The pair with the key of %v has been removed from the ConcurrentMap instance]\n",k1)v1, ok := cMap.Load(k1)fmt.Printf("The result of Load: %v, %v (key: %v)\n",v1, ok, k1)v1 = pairs[1].vactual1, loaded1 := cMap.LoadOrStore(k1, v1)fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",actual1, loaded1, k1, v1)cMap.Range(func(key, value interface{}) bool {fmt.Printf("The result of an iteration in Range: %d, %s\n",key, value)return true})}
}

笔记源码

GitHub - MingsonZheng/go-core-demo: Go语言核心36讲学习源码

Go语言核心36讲(Go语言实战与应用十三)--学习笔记相关推荐

  1. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  2. 《go语言核心36讲》栏目心得

    本人在学习Python的时候就听说过Go语言,上个月凭借着go语言的官网将go语言学习了,知识点零零散散,以至于做一个并发版的网络爬虫,在数据的存储和展示时花费了大量的时间,但几天有幸看到go语言大牛 ...

  3. (转)Go语言核心36讲之Go语言入门基础知识

    转载于:https://www.cnblogs.com/forever521Lee/p/10239919.html

  4. Go语言核心36讲笔记——程序实体那些事

    1.变量有两种声明方式: var name string //完整变量声明 name := "yan" //短变量声明 2.如果内层变量使用:=去定义,会产生内部变量 ,但不影响外 ...

  5. C语言中rvfsu是什么意思,C语言第4讲-C语言编程三结构.ppt

    C语言第4讲-C语言编程三结构 C 语句可以分为5类: 1.控制语句----9种 2.函数调用语句 3.表达式语句 4.空语句---什么也不做 5.复合语句----用{}括起来 复合语句:用 {-}括 ...

  6. 拉勾启源老师mysql讲义,【拉勾教育数据分析实战训练营】--Tableau学习笔记-重点回顾1...

    [拉勾教育数据分析实战训练营]--Tableau学习笔记-重点回顾1 [拉勾教育数据分析实战训练营]--Tableau学习笔记-重点回顾1 以下是我搜罗的一些官方优秀case分享: 1.https:/ ...

  7. 《Node.js开发实战详解》学习笔记

    <Node.js开发实战详解>学习笔记 --持续更新中 一.NodeJS设计模式 1 . 单例模式 顾名思义,单例就是保证一个类只有一个实例,实现的方法是,先判断实例是否存在,如果存在则直 ...

  8. 杨晓峰Java核心36讲学习笔记

    最近在极客时间上订阅了Oracle首席工程师杨晓峰的Java核心技术36讲, 接下来会对每一课的学习: 记下学习笔记 有不懂的地方继续深入 一些思考或者总结. 下面从第一课开始,Exception和E ...

  9. c语言imagesize怎么用里面的参数如何填写,【学习笔记】【C语言】sizeof

    1.用来计算一个变量或者一个常量.一种数据类型所占的内存字节数. 2.sizeof一共有3种形式 1>sizeof( 变量\常量 ) sizeof(10); char c = 'a'; size ...

最新文章

  1. python基于值得内存_为什么说Python采用的是基于值的内存管理模式
  2. 计算机无法安装win10,win10系统电脑提示“windows安装无法继续”的还原步骤
  3. Leetcode114二叉树转链表-树中修改
  4. 刘汝佳训练指南——数论专题知识点总结:
  5. Django shell保存数据
  6. 系统技巧之如何巧妙的整理磁盘碎片
  7. python在线学习直播-Python在线学习最有效马哥开启全网独家全程直播课
  8. python xlutils教程_python xlutils复制表格使用方法
  9. 了解java虚拟机mdash;串行回收器(6)
  10. 一个基于verilog的FPGA 的LCD 1602 显示程序
  11. GDB调试 ORBSLAM3
  12. c语言增量pi调节程序,增量式PI控制函数源代码(C语言)
  13. 云计算就业方向有哪些 未来的发展前景怎么样
  14. 乱谈那些个著名的科技互联网公司和产品名字
  15. Ubuntu安装企业微信解决中文字体乱码的问题
  16. 逆向破解思路和获取app的代码,脱壳操作(一)
  17. PyTorch数据归一化处理:transforms.Normalize及计算图像数据集的均值和方差
  18. 学习RNN-part2
  19. 哎呦,最近流行校内体哦--每个用 power shell 的程序员,上辈子都是折翼的天使
  20. 面试题:高频前端面试题之CSS篇

热门文章

  1. C/C++语言:依次取出一个正整数的各位数字,及其变种形式
  2. 客服要知道的女性消费心理
  3. 初三计算机模拟考试在房考吗,中考临近,初三学生想知道中考难还是模拟考试难,你知道哪个难吗...
  4. 致敬马克龙访华?法国品牌手机接入鸿蒙!?---转自百度新闻
  5. mysql新增,更新,批量插入操作
  6. 岭回归中GCV,HKB,L-W的解释
  7. SSM学习——SpringBoot初学注意事项(14)
  8. 触发器——SR锁存器
  9. 负面漩涡中的二手车平台,原本有着另一副面孔
  10. c#检测端口是否被占用