golang中的map并发读写问题: Golang 协程并发使用 Map 的正确姿势

Posted 禅与计算机程序设计艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang中的map并发读写问题: Golang 协程并发使用 Map 的正确姿势相关的知识,希望对你有一定的参考价值。

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", []int1, 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", []int1, 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 协程并发使用 Map 的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章

golang中map并发读写问题及解决方法

golang map并发读写异常导致服务崩溃

golang语言map的并发和排序

码住!Golang并发安全与引用传递总结

不得不知道的Golang之sync.Map解读!

Golang中sync.Map的实现原理