【问题标题】:sync.Map seems not safe concurrent read/writesync.Map 似乎不安全的并发读/写
【发布时间】:2021-03-24 02:28:36
【问题描述】:

测试 golang 标准包中的 sync.Map。并发读写似乎不安全。 怎么了?

测试代码:

package main

import (
    "log"
    "sync"
)

func main() {
    var m sync.Map
    m.Store("count", 0)

    var wg sync.WaitGroup

    for numOfThread := 0; numOfThread < 10; numOfThread++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for i := 0; i < 1000; i++ {
                value, ok := m.Load("count")
                if !ok {
                    log.Println("load count error")
                } else {
                    v, _ := value.(int)
                    m.Store("count", v+1)
                }
            }
        }()
    }

    log.Println("threads starts")
    wg.Wait()

    value, ok := m.Load("count")
    if ok {
        v, _ := value.(int)
        log.Printf("final count: %d", v)
    }

    log.Println("all done")
}

https://play.golang.org/p/E-pw4iZUceB

结果应该是10000,但是得到的是随机数而不是10000:

2009/11/10 23:00:00 threads starts
2009/11/10 23:00:00 final count: 6696
2009/11/10 23:00:00 all done

【问题讨论】:

    标签: go concurrency


    【解决方案1】:

    你有一个竞争条件:

     value, ok := m.Load("count")
     ...
     v, _ := value.(int)
     m.Store("count", v+1)
    

    上面的 read-modify-store 不保护其他 goroutine 做同样的事情,因此其他 goroutine 执行的一些增量会被遗漏。

    sync.Map 保护对其成员的并发访问。这意味着,对 map 的写入不会导致其他 goroutine 读取不一致的 map。如果你读-修改-写,没有什么可以保护其他 goroutine 同时更新值。当您读取-修改-更新时,您需要一个互斥锁来保护对地图的访问。

    【讨论】:

    • 谢谢,我明白了。所以如果我使用sync.Map来存储用户的API请求计数,它应该添加额外的互斥锁来保护read-plusone-update周期,我认为它可能会降低性能,这种场景有什么成熟的解决方案吗?
    • 使用常规映射map[string]*uint64,并使用sync/atomic 增加映射值而不使用任何锁。
    【解决方案2】:
    package main
    
    import (
        "log"
        "sync"
    )
    
    func main() {
        var m sync.Map
        m.Store("count", 0)
    
        var wg sync.WaitGroup
        var mu *sync.Mutex
    
        for numOfThread := 0; numOfThread < 10; numOfThread++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                for i := 0; i < 1000; i++ {
                    mu.Lock()
                    value, ok := m.Load("count")
                    if !ok {
                        log.Println("load count error")
                    } else {
                        v, _ := value.(int)
                        m.Store("count", v+1)
                    }
                   mu.Unlock()
                }
            }()
        }
    
        log.Println("threads starts")
        wg.Wait()
    
        value, ok := m.Load("count")
        if ok {
            v, _ := value.(int)
            log.Printf("final count: %d", v)
        }
    
        log.Println("all done")
    }
    

    读取和写入操作都是线程安全的,但您正在尝试更新插入或读取+写入操作。它不是线程安全的。修改代码使其线程安全。

    【讨论】:

    • 是的,此代码适用于 golang 的内置地图类型。使用sync.Map的原因是提供线程安全的读或写,它不提供线程安全的读+写,如原始问题所述。
    • ThinkGoodly 的观点是,sync.Map 是多余的,因为临界区现在由 sync.Mutex 保护。
    • 是的,我明白了,使用 sync.Map 没有任何好处。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-13
    • 2016-07-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多