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.Mutexsync.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 不是线程安全的,在并发场景中容易引发数据竞态或崩溃。
  • 解决方案
    1. 使用 sync.Mutexsync.RWMutex 保护原生 map
    2. 使用线程安全的 sync.Map
  • 选择方案
    • 如果读多写少,建议使用 sync.Map
    • 如果需要精确控制并发,且写操作较多,建议使用 sync.Mutex

了解 map 的并发安全性以及使用合适的方案,是编写高效、稳定的 Go 并发程序的关键。

发表评论

后才能评论