关于php:为什么-Go-map-和-slice-是非线程安全的

9次阅读

共计 4489 个字符,预计需要花费 12 分钟才能阅读完成。

大家好,我是煎鱼。

初入 Go 语言的大门,有不少的小伙伴会疾速的 3 天精通 Go,5 天上手我的项目,14 天上线业务迭代,21 天排查、定位问题,顺带捎个检查报告。

其中最常见的初级错误,Go 面试较最爱问的问题之一:


(来自读者的发问)

为什么在 Go 语言里,map 和 slice 不反对并发读写,也就是是非线程平安的,为什么不反对?

见招拆招后,紧接着就会开始探讨如何让他们俩”朋友“反对并发读写?

明天咱们这篇文章就来理一理,理解其前因后果,一起吸鱼学懂 Go 语言。

非线程平安的例子

slice

咱们应用多个 goroutine 对类型为 slice 的变量进行操作,看看后果会变的怎么样。

如下:

func main() {var s []string
 for i := 0; i < 9999; i++ {go func() {s = append(s, "脑子进煎鱼了")
  }()}

 fmt.Printf("进了 %d 只煎鱼", len(s))
}

输入后果:

// 第一次执行
进了 5790 只煎鱼
// 第二次执行
进了 7370 只煎鱼
// 第三次执行
进了 6792 只煎鱼

你会发现无论你执行多少次,每次输入的值大概率都不会一样。也就是追加进 slice 的值,呈现了笼罩的状况。

因而在循环中所追加的数量,与最终的值并不相等。且这种状况,是不会报错的,是一个呈现率不算高的隐式问题。

这个产生的次要起因是程序逻辑自身就有问题,同时读取到雷同索引位,天然也就会产生笼罩的写入了。

map

同样针对 map 也如法炮制一下。反复针对类型为 map 的变量进行写入。

如下:

func main() {s := make(map[string]string)
 for i := 0; i < 99; i++ {go func() {s["煎鱼"] = "吸鱼"
  }()}

 fmt.Printf("进了 %d 只煎鱼", len(s))
}

输入后果:

fatal error: concurrent map writes

goroutine 18 [running]:
runtime.throw(0x10cb861, 0x15)
        /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1117 +0x72 fp=0xc00002e738 sp=0xc00002e708 pc=0x1032472
runtime.mapassign_faststr(0x10b3360, 0xc0000a2180, 0x10c91da, 0x6, 0x0)
        /usr/local/Cellar/go/1.16.2/libexec/src/runtime/map_faststr.go:211 +0x3f1 fp=0xc00002e7a0 sp=0xc00002e738 pc=0x1011a71
main.main.func1(0xc0000a2180)
        /Users/eddycjy/go-application/awesomeProject/main.go:9 +0x4c fp=0xc00002e7d8 sp=0xc00002e7a0 pc=0x10a474c
runtime.goexit()
        /usr/local/Cellar/go/1.16.2/libexec/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc00002e7e0 sp=0xc00002e7d8 pc=0x1063fe1
created by main.main
        /Users/eddycjy/go-application/awesomeProject/main.go:8 +0x55

好家伙,程序运行会间接报错。并且是 Go 源码调用 throw 办法所导致的致命谬误,也就是说 Go 过程会中断。

不得不说,这个并发写 map 导致的 fatal error: concurrent map writes 谬误提醒。我有一个敌人,曾经看过少说几十次了,不同组,不同人 …

是个日经的隐式问题。

如何反对并发读写

对 map 上锁

实际上咱们依然存在并发读写 map 的诉求(程序逻辑决定),因为 Go 语言中的 goroutine 切实是太不便了。

像是个别写爬虫工作时,根本会用到多个 goroutine,获取到数据后再写入到 map 或者 slice 中去。

Go 官网在 Go maps in action 中提供了一种简略又便当的形式来实现:

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

这条语句申明了一个变量,它是一个匿名构造(struct)体,蕴含一个原生和一个嵌入读写锁 sync.RWMutex

要想从变量中中读出数据,则调用读锁:

counter.RLock()
n := counter.m["煎鱼"]
counter.RUnlock()
fmt.Println("煎鱼:", n)

要往变量中写数据,则调用写锁:

counter.Lock()
counter.m["煎鱼"]++
counter.Unlock()

这就是一个最常见的 Map 反对并发读写的形式了。

sync.Map

前言

尽管有了 Map+Mutex 的极简计划,然而也依然存在肯定问题。那就是在 map 的数据量十分大时,只有一把锁(Mutex)就十分可怕了,一把锁会导致大量的抢夺锁,导致各种抵触和性能低下。

常见的解决方案是分片化,将一个大 map 分成多个区间,各区间应用多个锁,这样子锁的粒度就大大降低了。不过该计划实现起来很简单,很容易出错。因而 Go 团队到比拟为止暂无举荐,而是采取了其余计划。

该计划就是在 Go1.9 起反对的 sync.Map,其反对并发读写 map,起到一个补充的作用。

具体介绍

Go 语言的 sync.Map 反对并发读写 map,采取了“空间换工夫”的机制,冗余了两个数据结构,别离是:read 和 dirty,缩小加锁对性能的影响:

type Map struct {
 mu Mutex
 read atomic.Value // readOnly
 dirty map[interface{}]*entry
 misses int
}

其是专门为 append-only 场景设计的,也就是适宜读多写少的场景。这是他的长处之一。

若呈现写多 / 并发多的场景,会导致 read map 缓存生效,须要加锁,抵触变多,性能急剧下降。这是他的重大毛病。

提供了以下罕用办法:

func (m *Map) Delete(key interface{})
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
func (m *Map) Range(f func(key, value interface{}) bool)
func (m *Map) Store(key, value interface{})
  • Delete:删除某一个键的值。
  • Load:返回存储在 map 中的键的值,如果没有值,则返回 nil。ok 后果示意是否在 map 中找到了值。
  • LoadAndDelete:删除一个键的值,如果有的话返回之前的值。
  • LoadOrStore:如果存在的话,则返回键的现有值。否则,它存储并返回给定的值。如果值被加载,加载的后果为 true,如果被存储,则为 false。
  • Range:递归调用,对 map 中存在的每个键和值顺次调用闭包函数 f。如果 f 返回 false 就进行迭代。
  • Store:存储并设置一个键的值。

理论运行例子如下:

var m sync.Map

func main() {
 // 写入
 data := []string{"煎鱼", "咸鱼", "烤鱼", "蒸鱼"}
 for i := 0; i < 4; i++ {go func(i int) {m.Store(i, data[i])
  }(i)
 }
 time.Sleep(time.Second)

 // 读取
 v, ok := m.Load(0)
 fmt.Printf("Load: %v, %v\n", v, ok)

 // 删除
 m.Delete(1)

 // 读或写
 v, ok = m.LoadOrStore(1, "吸鱼")
 fmt.Printf("LoadOrStore: %v, %v\n", v, ok)

 // 遍历
 m.Range(func(key, value interface{}) bool {fmt.Printf("Range: %v, %v\n", key, value)
  return true
 })
}

输入后果:

Load: 煎鱼, true
LoadOrStore: 吸鱼, false
Range: 0, 煎鱼
Range: 1, 吸鱼
Range: 3, 蒸鱼
Range: 2, 烤鱼

为什么不反对

Go Slice 的话,次要还是索引位覆写问题,这个就不须要纠结了,势必是程序逻辑在编写上有显著缺点,自行改之就好。

但 Go map 就不大一样了,很多人认为是默认反对的,一个不小心就翻车,这么的常见。那凭什么 Go 官网还不反对,难不成太简单了,性能太差了,到底是为什么?

起因如下(via @go faq):

  • 典型应用场景:map 的典型应用场景是不须要从多个 goroutine 中进行平安拜访。
  • 非典型场景(须要原子操作):map 可能是一些更大的数据结构或曾经同步的计算的一部分。
  • 性能场景思考:若是只是为多数程序减少安全性,导致 map 所有的操作都要解决 mutex,将会升高大多数程序的性能。

汇总来讲,就是 Go 官网在通过了长时间的探讨后,认为 Go map 更应适配典型应用场景,而不是为了小局部状况,导致大部分程序付出代价(性能),决定了不反对。

总结

在明天这篇文章中,咱们针对 Go 语言中的 map 和 slice 进行了根本的介绍,也对不反对并发读者的场景进行了模仿展现。

同时也针对业内常见的反对并发读写的形式进行了讲述,最初剖析了不反对的起因,让咱们对整个前因后果有了一个残缺的理解。

不晓得你 在日常是否有遇到过 Go 语言中非线性平安的问题呢,欢送你在评论区留言和大家一起交换

若有任何疑难欢送评论区反馈和交换,最好的关系是相互成就 ,各位的 点赞 就是煎鱼创作的最大能源,感激反对。

文章继续更新,能够微信搜【脑子进煎鱼了】浏览,本文 GitHub github.com/eddycjy/blog 已收录,欢送 Star 催更。

正文完
 0