首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >golang中的map并发读写问题: Golang 协程并发使用 Map 的正确姿势

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

作者头像
一个会写诗的程序员
发布2022-09-28 09:11:02
发布2022-09-28 09:11:02
4.8K0
举报

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.

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

代码语言:javascript
复制
const (
  ...
    hashWriting  = 4 // a goroutine is writing to the map
    ...
)

type hmap struct {
    ...
    flags     uint8
    ...
}

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

代码语言:javascript
复制
// 在更新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不停地读

代码语言:javascript
复制
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)
}

立马产生错误

代码语言:javascript
复制
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.

代码语言:javascript
复制
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()

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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?
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-09-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • map 不是并发安全的
  • 第三方 map 包
  • sync.Map
  • 参考链接
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档