美文网首页
golang中的map并发读写问题: Golang 协程并发使用

golang中的map并发读写问题: Golang 协程并发使用

作者: 光剑书架上的书 | 来源:发表于2022-09-20 12:05 被阅读0次

    map 不是并发安全的

    官方的faq里有说明,考虑到有性能损失,map没有设计成原子操作,在并发读写时会有问题。

    Map access is unsafe only when updates are occurring. As long as all goroutines are only reading—looking up elements in the map, including iterating through it using a for range loop—and not changing the map by assigning to elements or doing deletions, it is safe for them to access the map concurrently without synchronization.

    查看源码,进一步立即实现机制

    const (
      ...
        hashWriting  = 4 // a goroutine is writing to the map
        ...
    )
    
    type hmap struct {
        ...
        flags     uint8
        ...
    }
    

    map是检查是否有另外线程修改h.flag来判断,是否有并发问题。

    // 在更新map的函数里检查并发写
        if h.flags&hashWriting == 0 {
            throw("concurrent map writes")
        }
        
    // 在读map的函数里检查是否有并发写
        if h.flags&hashWriting != 0 {
            throw("concurrent map read and map write")
        }
    

    测试并发问题的例子:一个goroutine不停地写,另一个goroutine不停地读

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        c := make(map[string]int)
        go func() { //开一个goroutine写map
            for j := 0; j < 1000000; j++ {
                c[fmt.Sprintf("%d", j)] = j
            }
        }()
        go func() { //开一个goroutine读map
            for j := 0; j < 1000000; j++ {
                fmt.Println(c[fmt.Sprintf("%d", j)])
            }
        }()
        time.Sleep(time.Second * 20)
    }
    

    立马产生错误

    0
    fatal error: concurrent map read and map write
    
    goroutine 19 [running]:
    runtime.throw(0x10d6ea4, 0x21)
            /usr/local/go/src/runtime/panic.go:774 +0x72 fp=0xc00009aef0 sp=0xc00009aec0 pc=0x10299c2
    runtime.mapaccess1_faststr(0x10b41e0, 0xc000066180, 0x116ae71, 0x1, 0x1)
            /usr/local/go/src/runtime/map_faststr.go:21 +0x44f fp=0xc00009af60 sp=0xc00009aef0 pc=0x100ffff
    main.main.func2(0xc000066180)
    

    加sync.RWMutex来保护map

    This statement declares a counter variable that is an anonymous struct containing a map and an embedded sync.RWMutex.

    var counter = struct{
        sync.RWMutex
        m map[string]int
    }{m: make(map[string]int)}
    To read from the counter, take the read lock:
    
    counter.RLock()
    n := counter.m["some_key"]
    counter.RUnlock()
    fmt.Println("some_key:", n)
    To write to the counter, take the write lock:
    
    counter.Lock()
    counter.m["some_key"]++
    counter.Unlock()
    
    

    针对上面有并发问题的测试例子,可以修改成以下代码:

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    func main() {
        var c = struct {
            sync.RWMutex
            m map[string]int
        }{m: make(map[string]int)}
    
        go func() { //开一个goroutine写map
            for j := 0; j < 1000000; j++ {
                c.Lock()
                c.m[fmt.Sprintf("%d", j)] = j
                c.Unlock()
            }
        }()
        go func() { //开一个goroutine读map
            for j := 0; j < 1000000; j++ {
                c.RLock()
                fmt.Println(c.m[fmt.Sprintf("%d", j)])
                c.RUnlock()
            }
        }()
        time.Sleep(time.Second * 20)
    }
    
    

    第三方 map 包

    第三方包的实现都大同小异,基本上都是使用分离锁来实现并发安全的,具体分离锁来实现并发安全的原理可参考下面的延伸阅读

    concurrent-map

    m := cmap.New()
    //写
    m.Set("foo", "hello world")
    m.Set("slice", []int{1, 2, 3, 4, 5, 6, 7})
    m.Set("int", 1)
    //读
    m.Get("foo")  
    m.Get("slice") 
    m.Get("int")  
    go-concurrentMap
    
    m := concurrent.NewConcurrentMap()
    m.Put("foo", "hello world")
    m.Put("slice", []int{1, 2, 3, 4, 5, 6, 7})
    m.Put("int", 1)
    //读
    m.Get("foo")  
    m.Get("slice") 
    m.Get("int") 
    

    sync.Map

    sync.Map 是官方出品的并发安全的 map,他在内部使用了大量的原子操作来存取键和值,并使用了 read 和 dirty 二个原生 map 作为存储介质,具体实现流程可阅读相关源码。

    参考:
    https://learnku.com/articles/27691

    参考链接

    1. The Go Blog - Go maps in action

    2. Why are map operations not defined to be atomic?

    相关文章

      网友评论

          本文标题:golang中的map并发读写问题: Golang 协程并发使用

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