Golang的map 是线程安全的吗?
参考回答
Golang 的原生 map 不是线程安全的。在多 Goroutine 环境下,若同时对一个 map 进行读写操作,可能会导致程序崩溃,出现如下错误:
fatal error: concurrent map writes
如果需要在并发场景中使用 map,可以选择以下方式来保证线程安全:
1. 使用 互斥锁 (sync.Mutex) 手动保护 map 的访问。
2. 使用 sync.Map,这是 Go 提供的线程安全版本的 map。
详细讲解与拓展
1. 为什么原生 map 不是线程安全的?
map 在内部通过哈希表实现,多个 Goroutine 同时操作会导致状态竞争。例如:
– 一个 Goroutine 执行插入操作,扩展了 map 的哈希表。
– 同时另一个 Goroutine 进行读取操作,可能访问到了一个中间状态的 map。
这种情况下会导致数据的不一致,甚至直接崩溃。
示例:map 线程安全问题
package main
func main() {
m := make(map[int]int)
go func() {
for i := 0; i < 1000; i++ {
m[i] = i
}
}()
go func() {
for i := 0; i < 1000; i++ {
_ = m[i]
}
}()
select {} // 阻止主协程退出
}
运行这段代码可能会崩溃,抛出 fatal error: concurrent map writes。
2. 如何解决?
方案 1:使用 sync.Mutex
可以使用 sync.Mutex 或 sync.RWMutex 来保护 map 的读写操作。
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
m := make(map[int]int)
go func() {
for i := 0; i < 1000; i++ {
mu.Lock() // 加锁
m[i] = i
mu.Unlock() // 解锁
}
}()
go func() {
for i := 0; i < 1000; i++ {
mu.Lock() // 加锁
_ = m[i]
mu.Unlock() // 解锁
}
}()
select {}
}
sync.RWMutex 的优化:
如果读操作远多于写操作,可以使用读写锁 sync.RWMutex:
var rw sync.RWMutex
rw.RLock() // 读锁
value := m[key]
rw.RUnlock()
rw.Lock() // 写锁
m[key] = value
rw.Unlock()
方案 2:使用 sync.Map
从 Go 1.9 开始,标准库引入了 sync.Map,它是线程安全的 map,不需要显式加锁。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
go func() {
for i := 0; i < 1000; i++ {
m.Store(i, i) // 写入
}
}()
go func() {
for i := 0; i < 1000; i++ {
if val, ok := m.Load(i); ok { // 读取
fmt.Println(val)
}
}
}()
select {}
}
sync.Map 的特点:
– 适合读多写少的场景。
– 性能在高并发读的情况下优于手动加锁的 map。
– API 比较简单,常用方法包括:
– Store(key, value):存储键值对。
– Load(key):获取值。
– Delete(key):删除键。
– Range(func(key, value) bool):遍历所有键值。
3. 性能对比
在高并发场景下,以下是常见方式的性能特征:
– 原生 map:最快,但不安全。
– sync.Mutex 加锁:安全,但性能会随着锁争用的增加而下降。
– sync.Map:线程安全,在读多写少的场景性能优于 sync.Mutex。
例如,以下是测试代码:
package main
import (
"sync"
"time"
)
func benchmarkSyncMap() {
var m sync.Map
for i := 0; i < 100000; i++ {
go m.Store(i, i)
go m.Load(i)
}
}
func benchmarkMutexMap() {
var mu sync.Mutex
m := make(map[int]int)
for i := 0; i < 100000; i++ {
go func(i int) {
mu.Lock()
m[i] = i
mu.Unlock()
}(i)
go func(i int) {
mu.Lock()
_ = m[i]
mu.Unlock()
}(i)
}
}
func main() {
start := time.Now()
benchmarkSyncMap()
elapsedSyncMap := time.Since(start)
start = time.Now()
benchmarkMutexMap()
elapsedMutexMap := time.Since(start)
println("sync.Map:", elapsedSyncMap.Milliseconds(), "ms")
println("Mutex Map:", elapsedMutexMap.Milliseconds(), "ms")
}
结论:
– sync.Map 在读多写少的情况下性能较好。
– sync.Mutex 在写多场景下性能较优。
总结
- 原生
map不是线程安全的,在并发场景中容易引发数据竞态或崩溃。 - 解决方案:
- 使用
sync.Mutex或sync.RWMutex保护原生map。 - 使用线程安全的
sync.Map。
- 使用
- 选择方案:
- 如果读多写少,建议使用
sync.Map。 - 如果需要精确控制并发,且写操作较多,建议使用
sync.Mutex。
- 如果读多写少,建议使用
了解 map 的并发安全性以及使用合适的方案,是编写高效、稳定的 Go 并发程序的关键。