map是线程不安全的

最近发现了一个坑,原来Golang里面的map是线程不安全的,也就是说,如下代码,会出现问题:

package main

import (
    "fmt"
    "time"
)

func main() {
    m := make(map[string]string)
    go func() {
        for i := 0; i < 100000; i++ {
            v, ok := m["test"]
            if ok {
                fmt.Println(v)
            }
        }
    }()
    go func() {
        for i := 0; i < 100000; i++ {
            m["test"] = "test"
        }
    }()
    time.Sleep(2000)
}

什么?你不信?跑一下试试?你看看,panic了吧:

fatal error: concurrent map read and map write

goroutine 19 [running]:
runtime.throw({0xcdba49, 0x0})
        C:/Program Files/Go/src/runtime/panic.go:1198 +0x76 fp=0xc000049f20 sp=0xc000049ef0 pc=0xc63516
runtime.mapaccess2_faststr(0x0, 0x0, {0xcd5521, 0x4})
        C:/Program Files/Go/src/runtime/map_faststr.go:116 +0x3d4 fp=0xc000049f88 sp=0xc000049f20 pc=0xc406b4
main.main.func1()
        D:/WorkPlace/Source/Go/kakkk/main.go:12 +0x58 fp=0xc000049fe0 sp=0xc000049f88 pc=0xcbcd58
runtime.goexit()
        C:/Program Files/Go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc000049fe8 sp=0xc000049fe0 pc=0xc8d021
created by main.main
        D:/WorkPlace/Source/Go/kakkk/main.go:10 +0x65

为什么不支持

来看看Go FAQ上面的解释:

Why are map operations not defined to be atomic?

After long discussion it was decided that the typical use of maps did not require safe access from multiple goroutines, and in those cases where it did, the map was probably part of some larger data structure or computation that was already synchronized. Therefore requiring that all map operations grab a mutex would slow down most programs and add safety to few. This was not an easy decision, however, since it means uncontrolled map access can crash the program.

The language does not preclude atomic map updates. When required, such as when hosting an untrusted program, the implementation could interlock map access.

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.

As an aid to correct map use, some implementations of the language contain a special check that automatically reports at run time when a map is modified unsafely by concurrent execution.

翻译总结以下,就是以下几点:

  • 对于典型的使用场景,map不需要从多个goroutine中进行安全访问
  • 对于非典型的使用场景,map可能是一些已经同步的更大数据结构或计算的一部分
  • 为了性能考虑,没必要为非典型的使用场景而让所有读写都加个锁,降低性能

如何进行线程安全的读写

这里有三种方法

  1. 使用Mutex(互斥锁)
  2. 使用RWMutex(读写锁)
  3. 使用sync.Map

使用Mutex

package main

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

func main() {
    m := make(map[string]string)
    var l sync.Mutex
    go func() {
        for i := 0; i < 100000; i++ {
            l.Lock()
            v, ok := m["test"]
            if ok {
                fmt.Println(v)
            }
            l.Unlock()
        }
    }()
    go func() {
        for i := 0; i < 100000; i++ {
            l.Lock()
            m["test"] = "test"
            l.Unlock()
        }
    }()
    time.Sleep(2000)
}

使用RWMutex

package main

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

func main() {
    m := make(map[string]string)
    var l sync.RWMutex
    go func() {
        for i := 0; i < 100000; i++ {
            l.RLock()
            v, ok := m["test"]
            if ok {
                fmt.Println(v)
            }
            l.RUnlock()
        }
    }()
    go func() {
        for i := 0; i < 100000; i++ {
            l.Lock()
            m["test"] = "test"
            l.Unlock()
        }
    }()
    time.Sleep(2000)
}

使用sync.Map

package main

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

func main() {
    m := &sync.Map{}
    go func() {
        for i := 0; i < 100000; i++ {
            v, ok := m.Load("test")
            if ok {
                fmt.Println(v)
            }
        }
    }()
    go func() {
        for i := 0; i < 100000; i++ {
            m.Store("test", "test")
        }
    }()
    time.Sleep(2000)
}

如何选择

  • 在写入元素上,最慢的是sync.Map ,其次是Mutex,最快的是RwMutex
  • 在查找元素上,最慢的是Mutex,其次是RwMutex。最快的是sync.Map
  • 在删除元素上,最慢的是RwMutex,其次是Mutex,最快的是sync.Map

因此,sync.Map适用于读多写少的场景,MutexRWMutex适用于写入较多的场景

最后修改:2023 年 07 月 21 日
如果觉得我的文章对你有用,请随意赞赏