Go语言中的map是一种关联容器,它存储键值对,并且键是唯一的。在高并发情况下,如果多个goroutine同时对同一个map进行读写操作,可能会引发数据竞争,导致不可预知的行为,比如数据丢失、不一致性,甚至程序崩溃。
发生场景
如果有两个或更多的 goroutine 尝试同时修改同一个 map,比如添加、删除或更新键值对,就会发生并发安全问题,报错 fatal error: concurrent map writes
下图例子表示2个goroutine同时对一个map的同一个key的值进行自增10000次,最后打印出这个key的值。
运行结果有两种:
一种是报错
一种是输出一个不为20000的数
那么我们如何避免上图的情况,保证map的并发安全,最后输出正常的结果20000呢?以下由3种方案供选择。
解决方式
1. 使用互斥锁(Mutex)
互斥锁是一种基本的同步原语,可以确保在同一时刻只有一个goroutine可以访问map,有效地避免并发读写导致的数据竞争问题。在Go语言中,我们使用sync
包中的Mutex结构体
来实现互斥锁。
使用方法:
首先,声明互斥锁的全局变量。
接着,在map的读写操作前锁定互斥锁,在map的读写操作后解锁互斥锁。
最后,运行查看输出结果是否正常。
输出结果正常
2. 使用sync.map
sync.Map
是 Go 语言在 1.9 版本引入的一种并发安全的 map 类型,它提供了一种在多个 goroutine 中并发读写 map 的方式,而不需要程序员手动管理锁。sync.Map
内部通过特殊的设计来优化并发性能,它通常比使用互斥锁保护的普通 map 在高并发场景下有更好的性能表现。
package main
import (
"fmt"
"sync"
"time"
)
var counter sync.Map
func incrementCounter() {
for i := 0; i < 10000; i++ {
value, _ := counter.Load("count")
counter.Store("count", value.(int)+1)
}
}
func main() {
counter.Store("count", 0)
// 启动两个goroutine来增加counter
go incrementCounter()
go incrementCounter()
// 等待一段时间,让goroutine有机会运行
time.Sleep(time.Second)
// 打印counter的值
value, _ := counter.Load("count")
fmt.Println("Final Counter:", value)
}
需要注意的是,此方法只能保证程序不报错,但最后输出的结果不一定是20000 。
这是因为对于并发的计数操作,使用sync.Map
仍然无法保证原子性,因为每次加载和存储操作都是独立的,无法保证在同一时刻只有一个goroutine在执行这些操作。