大家好,我是煎鱼。

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

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

(来自读者提问)

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

见招拆招后,紧接着就会开始讨论如何让他们俩 ”冤家“ 支持并发读写?

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

非线程安全的例子

slice

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

如下:

输出结果:

你会发现无论你执行多少次,每次输出的值大概率都不会一样。也就是追加进 slice 的值,出现了覆盖的情况。

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

这个产生的主要原因是程序逻辑本身就有问题,同时读取到相同索引位,自然也就会产生覆盖的写入了。

map

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

如下:

输出结果:

throw
fatal error: concurrent map writes

是个日经的隐式问题。

如何支持并发读写

对 map 上锁

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

像是一般写爬虫任务时,基本会用到多个 goroutine,获取到数据后再写入到 map 或者 slice 中去。

Go 官方在 Go maps in action 中提供了一种简单又便利的方式来实现:

sync.RWMutex

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

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

这就是一个最常见的 Map 支持并发读写的方式了。

sync.Map

前言

虽然有了 Map+Mutex 的极简方案,但是也仍然存在一定问题。那就是在 map 的数据量非常大时,只有一把锁(Mutex)就非常可怕了,一把锁会导致大量的争夺锁,导致各种冲突和性能低下。

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

sync.Map

具体介绍

sync.Map
append-only

若出现写多/并发多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降。这是他的重大缺点。

提供了以下常用方法:

ff

实际运行例子如下:

输出结果:

为什么不支持

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 催更。