美文网首页
Go Map 为什么是非线程安全的?

Go Map 为什么是非线程安全的?

作者: Sun东辉 | 来源:发表于2022-05-16 10:21 被阅读0次

    Go map 默认是并发不安全的,同时对 map 进行并发读写的时,程序会 panic,原因如下:Go 官方经过长时间的讨论,认为 map 适配的场景应该是简单的(不需要从多个 gorountine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持。

    并发读写可能引发的问题

    func runWithPanic() {
        s := make(map[int]int)
        n := 100
        for i := 0; i < n; i++ {
            go func(i int) {
                s[i] = i
            }(i)
        }
        for i := 0; i <= n; i++ {
            go func(i int) {
                fmt.Printf("第 %d 个元素是 %v", i, s[i])
            }(i)
        }
        time.Sleep(time.Second)
        // fatal error: concurrent map writes
    }
    

    使用 sync.RWMutex 解决并发读写的问题

    func runWithSyncRWMutex() {
        var lock sync.RWMutex
        s := make(map[int]int)
        n := 100
        for i := 0; i < n; i++ {
            go func(i int) {
                lock.Lock()
                s[i] = i
                lock.Unlock()
            }(i)
        }
        for i := 0; i <= n; i++ {
            go func(i int) {
                lock.RLock()
                fmt.Printf("第 %d 个元素是%v;", i, s[i])
                lock.RUnlock()
            }(i)
        }
        time.Sleep(time.Second)
    }
    

    使用 sync.Map 解决并发读写的问题

    func RunWithSyncMap() {
        s := sync.Map{}
        n := 100
        for i := 0; i < n; i++ {
            go func(i int) {
                s.Store(i, i)
            }(i)
        }
        for i := 0; i <= n; i++ {
            go func(i int) {
                v, ok := s.Load(i)
                if ok {
                    fmt.Printf("第 %d 个元素是%v;", i, v)
                }
            }(i)
        }
        time.Sleep(time.Second)
    }
    

    实际上,sync.Map 也是通过加锁的方式实现并发安全的,sync.Map 源码的数据结构如下:

    type Map struct {
        mu Mutex
    
        // read contains the portion of the map's contents that are safe for
        // concurrent access (with or without mu held).
        //
        // The read field itself is always safe to load, but must only be stored with
        // mu held.
        //
        // Entries stored in read may be updated concurrently without mu, but updating
        // a previously-expunged entry requires that the entry be copied to the dirty
        // map and unexpunged with mu held.
        read atomic.Value // readOnly
    
        // dirty contains the portion of the map's contents that require mu to be
        // held. To ensure that the dirty map can be promoted to the read map quickly,
        // it also includes all of the non-expunged entries in the read map.
        //
        // Expunged entries are not stored in the dirty map. An expunged entry in the
        // clean map must be unexpunged and added to the dirty map before a new value
        // can be stored to it.
        //
        // If the dirty map is nil, the next write to the map will initialize it by
        // making a shallow copy of the clean map, omitting stale entries.
        dirty map[any]*entry
    
        // misses counts the number of loads since the read map was last updated that
        // needed to lock mu to determine whether the key was present.
        //
        // Once enough misses have occurred to cover the cost of copying the dirty
        // map, the dirty map will be promoted to the read map (in the unamended
        // state) and the next store to the map will make a new dirty copy.
        misses int
    }
    

    最后

    就像官方考虑的那样,我们在使用中,应尽量避免对 Map 进行并发读写,尝试通过其他方式解决问题,如数据解耦、分布执行、动态规划等,真的需要并发读写时,为避免产生并发读写的问题,请使用锁的机制进行控制

    相关文章

      网友评论

          本文标题:Go Map 为什么是非线程安全的?

          本文链接:https://www.haomeiwen.com/subject/gswiurtx.html