GoLand map中的并发问题——为什么会造成并发问题?该怎么解决?
问题提出
大家在使用map的时候,一定遇到过一个问题,由于map并不是线程安全,所以就会导致并发问题的出现。
下面先给大家演示一下这个问题:
func main() {
m := make(map[string]int, 2)
m["dd"] = 22
go func() {
for {
m["ff"] = 1
}
}()
go func() {
for {
_ = m["dd"]
}
}()
time.Sleep(1 * time.Hour)
}
会出现以下报错:
fatal error: concurrent map read and map write
为什么会抛出这个错误呢?
原因解析
具体原因
这个错误其实是“故意”设计给map的,在map的底层代码里写好的,为了避免map出现并发问题,用来确保数据的正确性的。
if h.flags&hashWriting != 0 {
fatal("concurrent map read and map write")
}
map这么设计有两点好处:
- 保证了map的运行性能,不使用锁机制降低了程序运行的开销
- 避免了map运行时造成不可预期的错误。比如map的渐进式扩容,在没有并发的情况下,开启扩容的前提一定是没有处于扩容状态,才能让每一步操作分担运行成本;如果并发操作,没有办法保证在下一次扩容之前完成了前一次的渐进扩容。
竞态检测器
不过大家可能会发现,map源码中是有一个竞态检测器的代码
// 如果启用了竞态检测并且h不为nil,进行竞态检测。
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := abi.FuncPCABIInternal(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.Key, key, callerpc, pc)
}
这个玩意干什么的呢?为啥有了这个东西还是不能避免并发问题呢?
- 这个东西在平时的生产环境是默认关闭的,开启需要在执行前输入"-race",像下面这样(如果运行有问题,见文末注释 )
go run -race main.go
- 这个东西只能用来检测并发程序中的竞态条件,并不能规避并发问题!
例如上面举例的并发错误代码,用-race运行结果是这样的
D:\GoLand 2024.1.1\program\test
go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c000020060 by goroutine 6:
runtime.mapassign_faststr()
D:/GoLand 2024.1.1/Go/src/runtime/map_faststr.go:203 +0x0
main.main.func1()
D:/GoLand 2024.1.1/program/test/main.go:12 +0x44
Previous read at 0x00c000020060 by goroutine 7:
runtime.mapaccess1_faststr()
D:/GoLand 2024.1.1/Go/src/runtime/map_faststr.go:13 +0x0
main.main.func2()
D:/GoLand 2024.1.1/program/test/main.go:17 +0x44
Goroutine 6 (running) created at:
main.main()
D:/GoLand 2024.1.1/program/test/main.go:10 +0xc5
Goroutine 7 (running) created at:
main.main()
D:/GoLand 2024.1.1/program/test/main.go:15 +0x130
==================
fatal error: concurrent map read and map write
goroutine 6 [running]:
main.main.func2()
D:/GoLand 2024.1.1/program/test/main.go:17 +0x45
created by main.main in goroutine 1
D:/GoLand 2024.1.1/program/test/main.go:15 +0x131
goroutine 1 [sleep]:
time.Sleep(0x34630b8a000)
D:/GoLand 2024.1.1/Go/src/runtime/time.go:195 +0x126
main.main()
D:/GoLand 2024.1.1/program/test/main.go:20 +0x145
goroutine 5 [runnable]:
main.main.func1()
D:/GoLand 2024.1.1/program/test/main.go:12 +0x45
created by main.main in goroutine 1
D:/GoLand 2024.1.1/program/test/main.go:10 +0xc6
exit status 2
WARNING: DATA RACE 就意味着发生了并发问题,还有并发问题的详细信息
如何解决并发问题呢?
有两种常用方法
方法一 : 使用sync.Mutex
我们可以使用互斥锁(sync.Mutex)来保护map的并发访问。在写入或读取map之前,我们需要获取锁,以确保同一时间只有一个goroutine可以访问map。
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]int),
}
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.Lock()
defer sm.mu.Unlock()
val, ok := sm.m[key]
return val, ok
}
func main() {
m := NewSafeMap()
m.Set("dd", 22)
go func() {
for {
m.Set("ff", 1)
}
}()
go func() {
for {
_, _ = m.Get("dd")
}
}()
time.Sleep(1 * time.Hour)
}
方法二: 使用sync.Map
我们首先了解一下sync.Map的常用方法:
用于添加或更新键值对。如果键已存在,它的值将被新值覆盖。
var m sync.Map
m.Store("exampleKey", "exampleValue")
用于获取键对应的值。如果键存在,返回键对应的值和true;如果不存在,返回nil和false。
if value, ok := m.Load("exampleKey"); ok {
fmt.Println("Value found:", value)
}
尝试从映射中加载键的值。如果键不存在,它将存储键值对到映射中。返回加载到的值(或存储的值)和一个布尔值,表示值是否被加载。
if actual, loaded := m.LoadOrStore("exampleKey", "newValue"); loaded {
fmt.Println("Value loaded:", actual)
} else {
fmt.Println("Value stored:", actual)
}
用于删除映射中的键及其对应的值。
m.Delete("exampleKey")
用于迭代映射中的所有键值对。它接受一个函数作为参数,该函数会被调用每个键值对。如果该函数返回false,迭代将停止。
m.Range(func(key, value interface{
}) bool {
fmt.Println("Key:", key, "Value:", value)
return true // 继续迭代
})
修改之前的代码
func main() {
var m sync.Map
m.Store("dd", 22)
go func() {
for {
m.Store("ff", 1)
}
}()
go func() {
for {
_, _ = m.Load("dd")
}
}()
time.Sleep(1 * time.Hour)
}
总结
- 为了保证性能,将map设置成了不可以并发
- 想要并发操作map,可以使用sync.Mutex 或者sync.Map
注释 : 竞态检测器
- 大家在使用"-race"启动的时候,可能会遇到下面的问题:
go: -race requires cgo; enable cgo by setting CGO_ENABLED=1
翻译:Go: -race要求Go;通过设置CGO_ENABLED=1开启cgo
解决方法 —— 使用env -w 修改环境变量的值:
go env -w CGO_ENABLED=1
- 之后可能还会出现下面的错误
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in %PATH%
先把gcc安装一下,配置一下环境变量就可以了~