golang-map是线程安全的吗?

IT小永 · · 12047 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

map

字典(map)它能存储的不是单一值的集合,而是键值对的集合。

什么是键值对?它是从英文 key-value pair 直译过来的一个词。顾名思义,一个键值对就代表
了一对键和值。一个“键”和一个“值”分别代表了一个从属于某一类型的独立值,把它们两个捆绑在一
起就是一个键值对了。在 Go 语言规范中,应该是为了避免歧义,他们将键值对换了一种称呼,
叫做:“键 - 元素对”。
Go 语言的字典类型其实是一个哈希表(hash table)的特定实现。在这个实现中,键和元素的
最大不同在于, 的类型是受限的,而元素对却可以是任意类型的。

代码:

    ma := make(map[string]string)

    ma["key"] = "value"// 存储

    value, ok := ma["key"]//获取值。ok:获取状态,value:ok=true说明有值,否则反之
    fmt.Println(value, ok)
    
    //遍历
    for key, value := range ma {
        fmt.Printf("key : %s, value:%s \n", key, value)
    }
    //删除
    delete(ma, "key")
    

map:不是线程安全的。在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是不安全
的。字典值本身可能会因这些操作而产生混乱,相关的程序也可能会因此发生不可预知的问题。

sync.Map

在 2017 年发布的 Go 1.9 中正式加入了并发安全的字典类型sync.Map。这个字典类型提供了一些常用的键值存取操作方法,并保证了这些操作的并发安全。同时,它的存、取、删等操作都可以基本保证在常数时间内执行完毕。换句话说,它们的算法复杂度与map类型一样都是O(1)的。在有些时候,与单纯使用原生map和互斥锁的方案相比,使用sync.Map可以显著地减少锁的争用。sync.Map本身虽然也用到了锁,但是,它其实在尽可能地避免使用锁。

代码:

    var ma sync.Map// 该类型是开箱即用,只需要声明既可
    ma.Store("key", "value") // 存储值
    ma.Delete("key") //删除值
    ma.LoadOrStore("key", "value")// 获取值,如果没有则存储
    fmt.Println(ma.Load("key"))//获取值
    
    //遍历
    ma.Range(func(key, value interface{}) bool {
        fmt.Printf("key:%s ,value:%s \n", key, value)
        //如果返回:false,则退出循环,
        return true
    })

扩展:

为什么map的键会有限制?

Go 语言规范规定,在键类型的值之间必须可以施加操作符==和!=。换句话说,键类型的值必须要支持判等操作。由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键

类型不能是这些类型。

map映射过程

哈希表中查找与某个键值对应的那个元素值,那么我们需要先把键值作为参数传给这个哈希表。哈希表会先用哈希函数(hash function)把键值转换为哈希值。哈希值通常是一个无符号的整数。一个哈希表会持有一定数量的桶(bucket),也可称之为哈希桶,这些哈希桶会均匀地储存其所属哈希表收纳的那些键 - 元素对。因此,哈希表会先用这个键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。随后,哈希表就会把相应的元素值作为结果返回。只要这个键 - 元素对存在于哈希表中就一定会被查找到。

如何判断那些类型作为字典的键比较合适?

在map查找的流程中,得知,“把键值转换为哈希值”以及“把要查找的键值与哈希桶中的键值做对比”, 是明显两个重要且比较耗时的操作。可以说:**求哈希和判等操作的速度越快,对应的类型就越适合作为键类型**。以求哈希的操作为例,宽度越小的类型速度通常越快。

基本类型:对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。类型的宽度是指它的单个值需要占用的字节数。比如,bool、int8和uint8类型的一个值需要占用的字节数都是1,因此这些类型的宽度就都是1。

高级类型。对数组类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度

为什么说并发安全字典在尽量避免使用锁?

我们从它的代码来解释

//sync.Map 包的结构
type Map struct {
   mu Mutex //锁
   
   /*
        由read字段的类型可知,sync.Map在替换只读字典的时候根本用不着锁。另外,这个只读字典
    在存储键值对的时候,还在值之上封装了一层。它先把值转换为了unsafe.Pointer类型的值,然后再把后者封     装,并储存在其中的原生字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子操作了。
   */
   read atomic.Value// 只读字典
   /*
        它存储键值对的方式与read字段中的原生字典一致,它的键类型也是interface{},并且同样是把值先做   转换和封装后再进行储存的
   */
   dirty map[interface{}]*entry//脏字典。
   misses int//重建的判断条件
}

查找键值对的时候:会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。否则,它才会在锁的保护下把键值对存储到脏字典中。这个时候,该键值对的“已删除”标记会被
抹去。

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

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

对于删除键值对,sync.Map会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。最后,sync.Map会把该键值对中指向值的那个指针置为nil,这是另一种逻辑删除的方式。

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

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

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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:IT小永

查看原文:golang-map是线程安全的吗?

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

12047 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传