普通map

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

map定义

map是一种无序的集合,对应的key (索引)会对应一个value(值),所以这个结构也称为关联数组或字典。

map在其他语言中hash、hash table等

  1. var mapName map[key]value
  • mapName 为 map 的变量名。
  • key 为键类型。
  • value 是键对应的值类型。

    map的使用和概念

    ```go package main

import “fmt”

func main() { maplist := make(map[string]int)//初始化内存了,想赋值就赋值 maplist[“three”] = 3 fmt.Println(maplist) }

  1. **map必须先初始化内存,后使用,也就是需要make一下,或者直接赋值一个空map**
  2. <a name="rcaiM"></a>
  3. ### map的容量
  4. 和数组不同的是,map可以根据新增的key-value动态的伸缩,因此不存在固定长度或者最大限制,但是也可以选择初始化容量的值
  5. ```go
  6. maplist := make(map[string]float, 100)

出于性能考虑,对于大的map或者快速扩张的map,最好先标明
用切片作为map的值

  1. maplist1 := make(map[int][]int)
  2. maplist2 := make(map[int]*[]int)

golang里的类型使用灵活,也可以任意组合,map里的值可以是struct,也可以是int、string、甚至是切片、数组。

map的使用

map的遍历

  1. scene := make(map[string]int)
  2. scene["route"] = 66
  3. scene["brazil"] = 4
  4. scene["china"] = 960
  5. for k, v := range scene {
  6. fmt.Println(k, v)
  7. }

map的删除和断言

  1. package main
  2. import "fmt"
  3. func main() {
  4. maplist := make(map[string]int)
  5. // 准备map数据
  6. maplist["LYY"] = 66
  7. maplist["520"] = 4
  8. maplist["666"] = 960
  9. delete(maplist, "666")
  10. for k, v := range maplist {
  11. fmt.Println(k, v)
  12. }
  13. }

map的坑

  1. package main
  2. import "fmt"
  3. func main() {
  4. m := map[int]struct{}{
  5. 1: {},
  6. 2: {},
  7. 3: {},
  8. 4: {},
  9. 5: {},
  10. }
  11. for k := range m {
  12. fmt.Println(k)
  13. }
  14. }
  15. //没有设置v值的时候,map的遍历是随机的,起始遍历是个随机值

并发安全 sync.Map

普通map不安全原因

官网解释:同一个变量在多个goroutine中访问需要保证其安全性

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. var TestMap map[string]string
  7. func init() {
  8. TestMap = make(map[string]string, 1)
  9. }
  10. func main() {
  11. for i := 0; i < 1000; i++ {
  12. go Write("aaa")
  13. go Read("aaa")
  14. go Write("bbb")
  15. go Read("bbb")
  16. }
  17. time.Sleep(5 * time.Second)
  18. }
  19. func Read(key string) {
  20. fmt.Println(TestMap[key])
  21. }
  22. func Write(key string) {
  23. TestMap[key] = key
  24. }
  25. //报错 fatal error: concurrent map writes

原因:因为map变量为 指针类型变量,并发写时,多个协程同时操作一个内存,类似于多线程操作同一个资源会发生竞争关系,共享资源会遭到破坏,因此golang 出于安全的考虑,抛出致命错误:fatal error: concurrent map writes。

解决方案

在读写操作的时候增加锁,删除时候除了加锁外,还需要增加断言避免出现错误

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var TestMap map[string]string
  8. func init() {
  9. TestMap = make(map[string]string, 1)
  10. }
  11. var lock sync.Mutex
  12. func main() {
  13. for i := 0; i < 1000; i++ {
  14. go Write("aaa")
  15. go Read("aaa")
  16. go Write("bbb")
  17. go Read("bbb")
  18. }
  19. time.Sleep(3 * time.Second)
  20. }
  21. func Read(key string) {
  22. lock.Lock()
  23. defer lock.Unlock()
  24. fmt.Println(TestMap[key])
  25. }
  26. func Write(key string) {
  27. lock.Lock()
  28. TestMap[key] = key
  29. lock.Unlock()
  30. }
  1. ...
  2. aaa
  3. bbb
  4. aaa
  5. bbb
  6. aaa
  7. bbb
  8. aaa
  9. bbb
  10. aaa
  11. bbb
  12. 进程 已完成,退出代码为 0

sync.Map包

Go语言普通map不是线程安全的,无法应用于Go语言的高并发场景。
Go语言原生实现了一种线程安全的sync.Map,可以实现并发的读写,并且性能比map+锁的机制的性能要高许多

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. func main() {
  7. m := sync.Map{}
  8. m.Store("a", 11)
  9. m.Store("b", 22)
  10. m.Store("c", 33)
  11. m.Store("d", 44)
  12. //读取数据
  13. fmt.Println(m.Load("a")) // 11 true
  14. m.Range(func(key, value any) bool {
  15. fmt.Println(key, value)
  16. return true
  17. })
  18. /**
  19. a 11
  20. b 22
  21. c 33
  22. d 44
  23. */
  24. }

sync.Map的底层结构

  1. type Map struct {
  2. mu Mutex
  3. // 基本上你可以把它看成一个安全的只读的map
  4. // 它包含的元素其实也是通过原子操作更新的,但是已删除的entry就需要加锁操作了
  5. read atomic.Value // readOnly
  6. // 包含需要加锁才能访问的元素
  7. // 包括所有在read字段中但未被expunged(删除)的元素以及新加的元素
  8. dirty map[interface{}]*entry
  9. // 记录从read中读取miss的次数,一旦miss数和dirty长度一样了,就会把dirty提升为read,并把dirty置空
  10. misses int
  11. }
  12. type readOnly struct {
  13. m map[interface{}]*entry
  14. amended bool // 当dirty中包含read没有的数据时为true,比如新增一条数据
  15. }
  16. // expunged是用来标识此项已经删掉的指针
  17. // 当map中的一个项目被删除了,只是把它的值标记为expunged,以后才有机会真正删除此项
  18. var expunged = unsafe.Pointer(new(interface{}))
  19. // entry代表一个值
  20. type entry struct {
  21. p unsafe.Pointer // *interface{}
  22. }

Map 的定义中,read 字段通过 atomic.Values 存储被高频读的 readOnly 类型的数据。dirty 存储

总结

sync.Map底层使用了两个map分离了map的扩容问题,分别是read map 和dirty map,read map主要用于对key的查找和修改,不会引发扩容的操作,dirty map主要用于新增数据,可能会引发扩容,这时候需要加锁。

优点

使用两个map将进行读写和追加分离,线程安全的,在读多追加少的场景具有较高的性能。

缺点

不适用于大量追加的场景,在追加的时候在read map中查找不到数据的时候,会进行加锁对dirty map进行操作,并进行提升和重构。但是,在 dirty map 刚被提升后,将 read map 复制到新的 dirty map中,在存在大量的数据的情况下复制操作会阻塞所有的协程极大的影响性能。

参考