当前位置: 首页 > 后端技术 > PHP

为什么Go的map和slice不是线程安全的?

时间:2023-03-29 19:37:01 PHP

大家好,我是炸鱼。初入Go语言,很多小伙伴3天快速掌握Go,5天上手项目,14天上线业务迭代,21天排查定位问题,并带来反思报告道路。最常见的初级错误之一,Go面试中最常被问到的问题之一:(读者提问)为什么在Go语言中,map和slice不支持并发读写,即非线程安全,为什么不支持吗?见招拆招,我们马上开始讨论如何让这两个“敌人”支持并发读写?今天我们就来看看这篇文章,了解它的前因后果,一起吃鱼学习理解Go语言。非线程安全的示例slice我们使用多个goroutine对slice类型的变量进行操作,看看结果会如何变化。如下:funcmain(){vars[]stringfori:=0;我<9999;i++{gofunc(){s=append(s,"我的脑子在炸鱼")}()}fmt.Printf("输入了%d条炸鱼",len(s))}输出结果://5790friedfisheswereenteredinthefirstexecution//7370friedfisheswereenteredinthesecondexecution//第三次执行输入6792friedfish后,你会发现无论执行多少次,每个输出值的概率都不会是相同的。也就是说,添加到切片的值被覆盖。因此,循环中添加的数字不等于最终值。而且这种情况下不会报错,属于隐性问题,发生率很低。这主要是程序逻辑本身有问题,同时读取同一个索引位时,自然会发生覆盖。地图也为地图做同样的事情。重复写入映射类型的变量。如下:funcmain(){s:=make(map[string]string)fori:=0;我<99;i++{gofunc(){s["friedfish"]="suctionfish"}()}fmt.Printf("Entered%dfriedfish",len(s))}输出结果:fatalerror:concurrentmapwritesgoroutine18[running]:runtime.throw(0x10cb861,0x15)/usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1117+0x72fp=0xc00002e738sp=0xc00002e708pc=0x1032472runtime.mapassign_faststr(0x10b3360,0xc0000a2180,0x10c91da,0x6,0x0)/usr/local/Cellar/go/1.16.2/libexec/src/runtime/map_faststr.go:211+0x3f1fp=0xc00002pc7a0sp=01xc7010main.func1(0xc0000a2180)ject/Users/maineddycjy/goome-application:9+0x4cfp=0xc00002e7d8sp=0xc00002e7a0pc=0x10a474cruntime.goexit()/usr/local/Cellar/go/1.16.2/src/libtimec/asm_amd64.s:1371+0x1fp=0xc00002e7e0sp=0xc00002e7d8pc=0x1063fe1createdbymain.main/Users/eddycjy/go-application/awesomeProject/main.go:8+0x55好家伙,程序会直接报错它运行。并且是Go源码调用throw方法导致的致命错误,也就是说Go进程会被中断。不得不说致命错误:concurrentmapwritesconcurrentlywritethemap导致的错误信息。我有个朋友看过几十遍了,不同的群体,不同的人。。。是日经隐含的问题。如何支持并发读写锁定map其实我们还是有并发读写map的诉求的(由程序逻辑决定),因为Go语言中的goroutine实在是太方便了。比如写爬虫任务,基本都是用多个goroutine,拿到数据后,写成map或者slice。Go官方在Gomapsinaction中提供了一种简单方便的实现方式:varcounter=struct{sync.RWMutexmmap[string]int}{m:make(map[string]int)}声明一个变量,这是一个匿名结构(struct)体,其中包含一个原生的和一个嵌入式读写锁sync.RWMutex。从变量中读取数据,调用读锁:counter.RLock()n:=counter.m["friedfish"]counter.RUnlock()fmt.Println("friedfish:",n)来写入数据对一个变量,调用写锁:counter.Lock()counter.m["friedfish"]++counter.Unlock()这是Map支持并发读写最常见的方式。sync.MapPreface虽然有Map+Mutex的极简方案,但是还是存在一些问题。也就是当map的数据量非常大的时候,只有一把锁(Mutex)是很吓人的。一把锁会造成很多锁的争用,导致各种冲突,性能很差。一个常见的解决方案是分片,将一个大的map分成多个区间,每个区间使用多把锁,这样子锁的粒度就大大降低了。然而,该解决方案实施起来复杂并且容易出错。因此,Go团队至今没有推荐,而是采用了其他方案。本方案为Go1.9支持的sync.Map,支持并发读写map,起到补充作用。具体介绍Go语言的sync.Map,支持并发读写map。它采用“空间换时间”机制和冗余的两种数据结构,即:read和dirty,减少加锁对性能的影响:typeMapstruct{muMutexreadatomic.Value//readOnlydirtymap[interface{}]*entrymissesint}是专门为append-only场景设计的,也就是适合读多写少的场景。这是他的强项之一。如果写/并发场景很多,读map缓存会失效,需要加锁,会产生更多的冲突,性能会急剧下降。这是他的主要缺点。提供了以下常用方法:func(m*Map)Delete(keyinterface{})func(m*Map)Load(keyinterface{})(valueinterface{},okbool)func(m*Map)LoadAndDelete(keyinterface{})(valueinterface{},loadedbool)func(m*Map)LoadOrStore(key,valueinterface{})(actualinterface{},loadedbool)func(m*Map)Range(ffunc(key,valueinterface{})bool)func(m*Map)Store(key,valueinterface{})Delete:删除一个key的value。Load:返回存储在map中的key的值,如果没有值则返回nil。ok结果指示是否在映射中找到该值。LoadAndDelete:删除键的值,如果有则返回以前的值。LoadOrStore:返回键的现有值(如果存在)。否则,它存储并返回给定的值。如果加载了值,则加载结果为真,如果已存储,则为假。Range:递归调用,依次为映射中存在的每个键和值调用闭包函数f。如果f返回false,则停止迭代。存储:存储和设置键的值。实际操作示例如下:varmsync.Mapfuncmain(){//Writedata:=[]string{"Friedfish","Saltedfish","Grilledfish","Steamedfish"}fori:=0;我<4;i++{gofunc(iint){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,"Suckfish")fmt.Printf("LoadOrStore:%v,%v\n",v,ok)//遍历m.Range(func(key,valueinterface{})bool{fmt.Printf("Range:%v,%v\n",key,value)returntrue})}输出结果:Load:friedfish,trueLoadOrStore:suckedfish,falseRange:0,friedfishRange:1,suckedfishRange:3,steamedfishRange:2,为什么烤鱼不支持GoSlice主要是索引位的覆盖。无需为此担心。肯定是程序逻辑有明显的缺陷,自己改正就好了。但是Go地图不同。很多人认为它是默认支持的,一不小心翻车就是这么常见。那为什么Go官方不支持呢?会不会太复杂,性能太差?为什么?原因如下(来自@gofaq):典型使用场景:地图的典型使用场景是不需要从多个goroutine进行安全访问的场景。非典型场景(需要原子操作):地图可能是一些更大的数据结构的一部分或已经同步的计算。性能场景考虑:如果只增加少数几个程序的安全性,导致所有map操作都处理mutex会降低大部分程序的性能。综上所述,经过长时间的讨论,Go官方认为Gomap应该更适合典型的使用场景,而不是针对少数情况,导致大部分程序付出代价(性能),决定不支持。小结在今天的文章中,我们对Go语言中的map和slice做了一个基本的介绍,同时也模拟了一个不支持并发读取的场景。同时也介绍了目前业界常见的支持并发读写的方式,最后分析了不支持的原因,让大家对整个前因后果有了一个完整的认识。不知道大家在日常生活中有没有遇到过Go语言非线性安全的问题。欢迎大家在评论区留言与大家交流!如有任何问题,欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中,可微信搜索【脑补炸鱼】阅读,本文已收录在GitHubgithub.com/eddycjy/blog,欢迎Star提醒。