再过5分钟你就能了解如何并发读写 map 啦

112 阅读2分钟
  • Go 语言原生 map 并不是线程安全的,不支持并发读写(并发读写map 里的数据会被写乱,容易导致 panic ),对它进行并发读写操作的时候,需要加锁。

  • 有以下两种方式可以实现 map 的并发读写:

  1. 使用 sync.Mutex 互斥锁。读和写的时候都使用互斥锁,性能相比 sync.RWMutex 会差一些。
  2. 使用 sync.RWMutex 读写锁。读的锁是可以共享的,但是写锁是独占的。性能相比 sync.Mutex 会好一些。

1. sync.Map

sync.Map 是一种并发安全的 map,在 Go 1.9 引入。

sync.Map 里面会先进行原子操作来读取 key,如果读取不到的时候,才会需要加锁。所以性能相比 sync.Mutexsync.RWMutex 会好一些。

sync.map 是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
sync.map 的零值是有效的,并且零值是一个空的 map。在第一次使用之后,不允许被拷贝。

一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错, 而使用 sync.Map 之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。

2. 使用方法(增删改查)

package main

import (
    "fmt"
    "sync"
)

func main() {
    var s_map sync.Map

    // 1. 写入
    s_map.Store("name", "kyle")
    s_map.Store("age", 18)

    // 2. 读取
    age, _ := s_map.Load("age")
    fmt.Println(age.(int))

    // 3. 遍历
    s_map.Range(func(key, value any) bool {
        name := key.(string)
        age := value.(int)
        fmt.Println(name, age)
        return true
    })

    // 4. 删除(删除其中的一个 key,再读这个 key,得到的就是 nil)
    s_map.Delete("name")
    name, ok := s_map.Load("name")
    fmt.Println(name, ok)

    // 5. 读取时候如果不存在则写入,如果存在则只输出原值
    s_map.LoadOrStore("name", "kyle")
    name, _ = s_map.Load("name")
    fmt.Println(name)
}

注意:在 sync.Map 中,keyvalue 都是 interface{} 类型的,也就是说,我们可以使用任意类型的 keyvalue。而不像 map,只能存在一种类型的 keyvalue。从这个角度来看,它的类型类似于 map[any]any

另外一个需要注意的是,Range 方法的参数是一个函数,这个函数如果返回 false,那么遍历就会停止。

sync.Map 适用于:

  • 当给定 key 的条目只写入一次但读取多次时,如在只会增长的缓存中。(读多写少)
  • 当多个 goroutine 读取、写入和覆盖不相交的键集的条目。(不同 goroutine 操作不同的 key)

对于写多的场景,会导致 read map 缓存失效,需要加锁,导致冲突变多;而且由于未命中 read map 次数过多,导致 dirty map 提升为 read map,这是一个 O(N) 的操作,会进一步降低性能。