title: golang之map并发访问
date: 2019-01-18 21:13:55
tags: [Go,Pearl]

categories: Tech


<font size=1 color=orange>

map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.

具体规则是:

(1)并发读没有问题;

(2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;

(3)读的时候发现其他 goroutine 在并发写,抛出fatal("concurrent map read and map write")

(4)写的时候发现其他 goroutine 在并发写,抛出fatal("concurrent map writes")

需要关注,此处并发读写会引发 fatal error,是一种比 panic 更严重的错误,无法使用 recover 操作捕获.

recover哪些情况下阻止不了程序崩溃?

</font>

来自 Golang map 实现原理




map不是并发安全的!


并发安全也称线程安全,如果在并发中出现了数据的丢失或错乱,就是并发不安全

golang的map不是并发安全的,并发对map读写可能会报fatal error


package main

import (
    "fmt"
    "time"
)

const N = 5

func main() {

    m := make(map[int]int)

    go func() {
        for i := 0; i < N; i++ {
            m[i] = i*10 + 6
        }
    }()

    go func() {
        for i := 0; i < N; i++ {
            fmt.Println(i, m[i])
        }
    }()

    time.Sleep(1e9 * 5)

}

输出为:

0 6
1 16
2 26
3 36
4 46

上面第一个协程对map写,第二个对map读,

注:如上代码中,调换两个协程的顺序,并不会对结果有影响


当 N 较大如等于1000时,该程序将报错:


fatal error: concurrent map read and map write

goroutine 18 [running]:
runtime.throw(0x10cb62d, 0x21)
    /usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc00002ef30 sp=0xc00002ef00 pc=0x10281e2
runtime.mapaccess1_fast64(0x10ab340, 0xc000084000, 0x0, 0x0)
    /usr/local/go/src/runtime/map_fast64.go:21 +0x1b3 fp=0xc00002ef58 sp=0xc00002ef30 pc=0x100eac3
main.main.func2(0xc000084000)
    /Users/shuangcui/go/note/map/1.go:22 +0x67 fp=0xc00002efd8 sp=0xc00002ef58 pc=0x1093c37
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc00002efe0 sp=0xc00002efd8 pc=0x1051b51
created by main.main
    /Users/shuangcui/go/note/map/1.go:20 +0x6a

goroutine 1 [sleep]:
runtime.goparkunlock(...)
    /usr/local/go/src/runtime/proc.go:307
time.Sleep(0x12a05f200)
    /usr/local/go/src/runtime/time.go:105 +0x159
main.main()
    /Users/shuangcui/go/note/map/1.go:26 +0x7d

goroutine 17 [runnable]:
main.main.func1(0xc000084000)
    /Users/shuangcui/go/note/map/1.go:16 +0x45
created by main.main
    /Users/shuangcui/go/note/map/1.go:14 +0x48
exit status 2

解决方案:

加锁实现


可以用sync.Mutex (互斥锁),或者sync.RWMutex(读写锁)来"重构"map,

即定义一个新的结构体,为该结构体实现两个方法,在方法里使用sync包的Mutex或RWMutex来做加锁保证并发安全.


package main

import (
    "fmt"
    "time"
    "sync"
)

const N = 1000

type Cmap struct {
    m    map[int]int
    lock *sync.RWMutex
}

func (c *Cmap) Get(key int) int {
    c.lock.RLock()
    defer c.lock.RUnlock()
    return c.m[key]
}

func (c *Cmap) Set(key, val int) {
    c.lock.Lock()
    defer c.lock.Unlock()
    c.m[key] = val*10 + 8
}

func main() {

    m := make(map[int]int)

    lock := new(sync.RWMutex)

    cm := Cmap{
        m:    m,
        lock: lock,
    }

    go func() {
        for i := 0; i < N; i++ {
            fmt.Println(i, cm.Get(i))
        }
    }()

    go func() {
        for i := 0; i < N; i++ {
            cm.Set(i, i)
        }
    }()

    time.Sleep(1e9 * 5)

}


直接使用sync.Map


从 go1.9 开始,新增了sync.Map,对并发情况下map的安全做了支持,可以对上面代码进行简化:

package main

import (
    "sync"
    "fmt"
    "time"
)

const N = 1000

func main() {
    var m sync.Map

    go func() {
        for i := 0; i < N; i++ {
            v, _ := m.Load(i) //读
            fmt.Println(i, v)
        }
    }()

    go func() {
        for i := 0; i < N; i++ {
            m.Store(i, i*10+4) //写
        }
    }()

    time.Sleep(5 * 1e9)

}


Store为写,Load为读

除去Store和Load两个方法,sync.Map还有几个方法:


  • 删除
func (m *Map) Delete(key interface{})
  • 遍历,类似于js的forEach循环
func (m *Map) Range(f func(key, value interface{}) bool)
  • 存或者取
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
这个方法有点绕,举例说下:
package main

import (
    "fmt"
    "sync"
)

func main() {

    var m sync.Map

    actual, loaded := m.LoadOrStore("k1", "v1")
    fmt.Println(actual, loaded) // v1 false

    actual, loaded = m.LoadOrStore("k1", "v1")
    fmt.Println(actual, loaded) // v1 true

    actual, loaded = m.LoadOrStore("k1", "v2")
    fmt.Println(actual, loaded) // v1 true

    actual, loaded = m.LoadOrStore("k2", "v2")
    fmt.Println(actual, loaded) // v2 false

}


LoadOrStore方法其实是先取,
如果有key的话就返回,没有再存储,即优先取,没有再存储;

该方法有两个返回值,第一个是键值,第二个是之前有无该键名(bool类型)

(和缓存的做法很类似:先从缓存中取,如果没有则从数据库中读,读回来存入缓存,下次就可以从缓存中取到了。)

本文由mdnice多平台发布


好文收藏
38 声望6 粉丝

好文收集