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这么设计有两点好处:

  1. 保证了map的运行性能,不使用锁机制降低了程序运行的开销
  2. 避免了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)
	}

这个玩意干什么的呢?为啥有了这个东西还是不能避免并发问题呢?

  1. 这个东西在平时的生产环境是默认关闭的,开启需要在执行前输入"-race",像下面这样(如果运行有问题,见文末注释
go run -race main.go
  1. 这个东西只能用来检测并发程序中的竞态条件,并不能规避并发问题!

例如上面举例的并发错误代码,用-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的常用方法:

  • Store(key, value interface{})

用于添加或更新键值对。如果键已存在,它的值将被新值覆盖。

var m sync.Map
m.Store("exampleKey", "exampleValue")
  • Load(key interface{}) (value interface{}, ok bool)

用于获取键对应的值。如果键存在,返回键对应的值和true;如果不存在,返回nil和false。

if value, ok := m.Load("exampleKey"); ok {
    fmt.Println("Value found:", value)
}
  • LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)

尝试从映射中加载键的值。如果键不存在,它将存储键值对到映射中。返回加载到的值(或存储的值)和一个布尔值,表示值是否被加载。

if actual, loaded := m.LoadOrStore("exampleKey", "newValue"); loaded {
    fmt.Println("Value loaded:", actual)
} else {
    fmt.Println("Value stored:", actual)
}
  • Delete(key interface{})

用于删除映射中的键及其对应的值。

m.Delete("exampleKey")
  • Range(f func(key, value interface{}) bool)

用于迭代映射中的所有键值对。它接受一个函数作为参数,该函数会被调用每个键值对。如果该函数返回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

注释 : 竞态检测器

  1. 大家在使用"-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
  1. 之后可能还会出现下面的错误
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in %PATH%

先把gcc安装一下,配置一下环境变量就可以了~

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值