G

[Golang] Map的底层实现原理与并发安全(待填坑)

RoLingG 其他 2024-10-20

Map的底层实现原理

Go语言采用的是哈希查找表,并且使用链表法解决哈希冲突

另外在面试里面,一般问到Map都会问它是否是并发安全的。

想要回答这类问题,只需要记住这一句话:Go语言提供的数据类型中,只有channel是并发安全的。

Map自身并发安全失败场景

func main() {
    //创建map
    m := make(map[int]string) 
    var wg sync.waitGroup
    wg.Add(2)
    
    //协程1写
    go func() {
        for true {
            m[0] = "xiaoming"
        }
        wg.Done()
    }()
    
    //协程2读
    go func() 
        for true {
            _ = m[0]
        }
        wg.Done()
    }()

    wg.wait()
    fmt . Print1n("结束")
}

编译后运行会报错:fatal error: concurrent map read and map write

原因

笔记来源:详解Go语言如何解决map并发安全问题

实现并发安全的Map

  1. 读写锁+map

    只需要在map进行读操作的时候,加读取锁后再进行读取;添加、更新、删除、遍历、获取长度这些操作加写锁再进行操作。

    每次进行这些map相关的操作都要进行加锁,我们通过学习Go语言的锁就会知道,读锁可以实现并发读,但是写锁只能单个写,阻塞其他对于此处的读写操作,是独占的。因此这样虽然能够保证数据的安全性,但也会影响程序的运行效率。

    所以这种加读写锁的并发安全做法一般适用于读多写少的场景。

  2. 分片+锁

    分片+锁,顾名思义就是将map分成多个片段,每个片段都由一个锁进行保护。

    数据通过散列函数将键分布到不同的小map片段中,从而减少锁的竞争,提高并发性能。

    这种方法适用于写操作较多,且写操作分布均匀的场景。分片map的原理是将map划分为多个片段,每个片段由一个单独的锁保护。当对map进行读写操作时,只需要锁定对应的map片段即可。这样可以大大提高并发读写的性能。

  3. sync.Map

    Go语言标准库提供的 sync.Map 类型,它不是为了替代普通的map,而是用于特定的场景。

    sync.Map 适用于读多写少的场景,并且写操作的key分布比较分散sync.Map内部使用了一个只读的map一个可写的map,读操作默认是无锁的,而写操作需要加锁

    misses(未命中读map的次数)超过一定阈值时,会将可写的map复制到只读map中。sync.Map 不能被复制,且在高频率写操作时性能可能下降。

    sync.Map 是 Go 语言中提供的一个并发安全的 map 类型,它允许多个 goroutine 安全地使用 map 进行读写操作而不需要使用互斥锁(mutex)。

    sync.Map 包里有增删改查等 Go 官方封装好的方法,用起来很方便嗷!

    具体的话还请看我之前写的:[[Golang基础] Sync包](https://rolingg.top/index.php/archives/103/)

读写锁+map 和 Sync.Map 相比较:

读写锁+map

优点

  1. 简单直观:使用sync.RWMutex 来保护一个普通的map,读操作使用读锁,写操作使用写锁。
  2. 适合读多写少的场景:允许多个读操作并发执行,而写操作是独占的,适合读操作远多于写操作的情况。

缺点

  1. 读操作性能受限:在高并发读操作下,虽然可以并发读取,但如果写操作频繁,读操作会因为锁的争用而受到性能影响。
  2. 写操作独占:写操作需要独占锁,这会阻塞所有的读和写操作,可能导致性能瓶颈。

sync.Map

优点

  1. 并发性能高:sync.Map 通过读写分离和延迟删除机制,优化了读多写少的场景下的并发性能。
  2. 空间换时间:使用两个map(read和dirty)来提高性能,减少了锁的使用,提高了并发读取的效率。
  3. 延迟删除:删除操作不会立即清理内存,而是标记为删除,直到下次同步时才清理,减少了锁的操作。

缺点

  1. 不适合大量写入的场景:在写操作频繁的场景下,sync.Map的性能可能下降,因为需要频繁地将dirty中的数据同步到read中。
  2. 内存使用高:由于维护了两个map,可能会增加内存的使用。
综上,sync.Map 对读操作加锁的优化更好,但是对于一些需要复制map,且对内存占用要求小的场景,用简单实现的读写锁+map会更好。

总结

写场景频繁,对性能要求也比较高的话,还是使用 分片+锁 的方式吧;其余的场景得看对内存的要求,对内存占用要求小的话,就用 读写锁+map,没这个需求的话,就用 sync.Map,方便快捷。

PREV
[Golang基础] Sync包

评论(0)

发布评论