首页 > golang > go语言中map拷贝 go map使用
2023
03-02

go语言中map拷贝 go map使用

背景

线上golang服务报错

fatal error: concurrent map iteration and map write


问题

map是线程不安全的,即使并发读写没有冲突也会报错 fatal error: concurrent map read and map write

package main 

import (
   "fmt"
   "time"
)

func main() {
   
   m := make(map[int]int)
   go func() {
      for {
         _ = m[1]
      }
   }()
   go func() {
      for {
         m[2] = 2
      }
   }()
   time.Sleep(time.Second * 1)

}

报错原因

image.png


image.png

写的时候设置了h.flags,获取的时候检查报错

image.png

写完会清除标志位


Maps are not safe for concurrent use:it's not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism. One common way to protect maps is with sync.RWMutex


map为引用类型,即使函数传值调用,行参副本依然指实参映射m, 所以多个goroutine并发写同一个映射m, 对于共享变量,资源,并发读写会产生竞争的, 故共享资源遭到破坏 

二.解决办法

1)使用读写锁sync.RWMutex:

var counter = struct{
   sync.RWMutex
   m map[string]int
}{m: make(map[string]int)}

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

counter.Lock()
counter.m["some_key"]++
counter.Unlock()


补充:对于不容易发现的并发问题,可以使用-race参数进行并发检测

go run -race main.go


如果对于加锁中间的操作比较耗时,比如,要循环map中每个元素做一些复杂操作,可以考虑 深度copy解决问题

// DeepCopyMap map[string]string 类型实现深拷贝
func DeepCopyMap(params map[string]string) map[string]string {
   result := map[string]string{}
   for k, v := range params {
      result[k] = v
   }
   return result
}

3)使用sync.Map

func main() {
   m := sync.Map{}
   m.Store(1, 1)
   m.Store("1", "1")
   fmt.Println(m.Load("1"))
   m.Delete("1")
   fmt.Println(m.Load("1"))
}


sync.Map是1.9才推荐的并发安全的map,除了互斥量以外,还运用了原子操作,所以在这之前,有必要了解下 Go语言——原子操作

go1.10\src\sync\map.go

entry分为三种情况:

从read中读取key,如果key存在就tryStore。

注意这里开始需要加锁,因为需要操作dirty。

条目在read中,首先取消标记,然后将条目保存到dirty里。(因为标记的数据不在dirty里)

最后原子保存value到条目里面,这里注意read和dirty都有条目。

总结一下Store:

这里可以看到dirty保存了数据的修改,除非可以直接原子更新read,继续保持read clean。

有了之前的经验,可以猜测下load流程:

与猜测的 区别 :

由于数据保存两份,所以删除考虑:

先看第二种情况。加锁直接删除dirty数据。思考下貌似没什么问题,本身就是脏数据。

第一种和第三种情况唯一的区别就是条目是否被标记。标记代表删除,所以直接返回。否则CAS操作置为nil。这里总感觉少点什么,因为条目其实还是存在的,虽然指针nil。

看了一圈貌似没找到标记的逻辑,因为删除只是将他变成nil。

之前以为这个逻辑就是简单的将为标记的条目拷贝给dirty,现在看来大有文章。

p == nil,说明条目已经被delete了,CAS将他置为标记删除。然后这个条目就不会保存在dirty里面。

这里其实就跟miss逻辑串起来了,因为miss达到阈值之后,dirty会全量变成read,也就是说标记删除在这一步最终删除。这个还是很巧妙的。

真正的删除逻辑:

很绕。。。。

golang 从 map 获取值时的值拷贝问题

我们知道 golang 中,slice, map, channel 是引用类型,函数之间传递都是以值拷贝的形式进行的,引用类型经过函数传递,依然是引用类型。

在上述例子中,我们从 map 中想拿出一个值,这个值是一个简单结构体,拿出这个值后,不确定这个值和 map 中的值是什么关系,如果不小心修改,是否会造成 map 值变更。

我们希望 golang 中更多的是值传递,这样能避免数据存储在堆上,造成 gc 负担。

可以看到,修改值后,map 中的值保持不变。说明 map 获取的值也是值传递出来的。

golang变量(二)——map和slice详解

衍生类型,interface{} , map, [] ,struct等

map类似于java的hashmap,python的dict,php的hash array。

常规的for循环,可以用for k,v :=range m {}. 但在下面清空有一个坑注意:

著名的map[string]*struct 副本问题

结果:

Go 中不存在引用传递,所有的参数传递都是值传递,而map是等同于指针类型的,所以在把map变量传递给函数时,函数对map的修改,也会实质改变map的值。

slice类似于其他语言的数组(list,array),slice初始化和map一样,这里不在重复

除了Pointer数组外,len表示使用长度,cap是总容量,make([]int, len, cap)可以预申请 比较大的容量,这样可以减少容量拓展的消耗,前提是要用到。

cap是计算切片容量,len是计算变量长度的,两者不一样。具体例子如下:

结果:

分析:cap是计算当前slice已分配的容量大小,采用的是预分配的伙伴算法(当容量满时,拓展分配一倍的容量)。

append是slice非常常用的函数,用于添加数据到slice中,但如果使用不好,会有下面的问题:

预期是[1 2 3 4 5 6 7 8 9 10], [1 2 3 4 5 6 7 8 9 10 11 12],但实际结果是:

注意slice是值传递,修改一下:

输出如下:

== 只能用于判断常规数据类型,无法使用用于slice和map判断,用于判断map和slice可以使用reflect.DeepEqual,这个函数用了递归来判断每层的k,v是否一致。

当然还有其他方式,比如转换成json,但小心有一些异常的bug,比如html编码,具体这个json问题,待后面在分析。


本文》有 0 条评论

留下一个回复