Go: 并发访问 Map Part III

发布于 2021-11-04 13:42 ,所属分类:软件编程学习资料

点击上方蓝色“Go语言中文网”,每天一起学 Go

Map

在上一篇文章 “Go: 通过源码研究 Map 的设计[1]” 中,我们讲述了 map 的内部实现。

Go blog[2] 中专门讲解 map 的文章明确地表明:

map 是非并发安全的[3]:并发读写 map 时,map 的行为是未知的。如果你需要使用并发执行的 Goroutine 同时读写 map,必须使用某种同步机制来协调访问。

然而,正如 FAQ[4] 中解释的,Google 提供了一些帮助:

作为一种纠正 map 使用方式的辅助手段,语言的某些实现包含了特殊的检查,当运行时的 map 被不安全地并发修改时,它会自动报告。

数据争用检测

我们可以从 Go 获得的第一个帮助就是数据争用检测。使用 -race 标记来运行你的程序或测试会让你了解潜在的数据争用。让我们看一个例子:

funcmain(){
m:=make(map[string]int,1)
m[`foo`]=1

varwgsync.WaitGroup

wg.Add(2)
Gofunc(){
fori:=0;i<1000;i++{
m[`foo`]++
}
}()
Gofunc(){
fori:=0;i<1000;i++{
m[`foo`]++
}
}()
wg.Wait()
}

在这个例子中,我们清晰地看到,在某一时刻,两个 Goroutine 尝试同时写入一个新值。下面是争用检测器的输出:

==================
WARNING:DATARACE
Readat0x00c00008e000byGoroutine6:
runtime.mapaccess1_faststr()
/usr/local/go/src/runtime/map_faststr.go:12+0x0
main.main.func2()
main.go:19+0x69

Previouswriteat0x00c00008e000byGoroutine5:
runtime.mapassign_faststr()
/usr/local/go/src/runtime/map_faststr.go:202+0x0
main.main.func1()
main.go:14+0xb8

争用检测器解释道,当第二个 Goroutine 正在读变量时,第一个 Goroutine 正在向同一个内存地址写一个新值。如果你想要了解更多,我建议你阅读我的一篇关于数据争用检测器[5]的文章。

本文是 Go语言中文网组织的 GCTT 翻译,发布在 Go语言中文网gongzhong号,转载请联系我们授权。

并发写入检测

Go 提供的另一个帮助是并发写入检测。让我们使用之前看到的那个例子。运行这个程序时,我们将看到一个错误:

fatalerror:concurrentmapwrites

在 map 结构的内部标志 flags 的帮助下,Go 处理了这次并发。当代码尝试修改 map 时(赋值,删除值或者清空 map),flags 的某一位会被置为 1:

funcmapdelete(t*maptype,h*hmap,keyunsafe.Pointer){
[...]
h.flags^=hashWriting

值为 4 的 hashWriting 会将相关的位置为 1。

^ 是一个异或操作,如果两个操作数的某一位的值不同,^ 将该位置为 1:

img

当操作结束时,该标志会被重置:

funcmapdelete(t*maptype,h*hmap,keyunsafe.Pointer){
[...]
h.flags&^=hashWriting
}

既然每个修改 map 的操作都设置了一个控制标志,那么通过检查这个标志的状态,就可以防止并发写入。这里是该标志的生命周期的例子:

funcmapdelete(t*maptype,h*hmap,keyunsafe.Pointer){
[...]
//ifanotherprocessiscurrentlywriting,throwerror
ifh.flags&hashWriting!=0{
throw("concurrentmapwrites")
}
[...]
//nooneiswriting,wecansetnowtheflag
h.flags^=hashWriting
[...]
//flagreset
h.flags&^=hashWriting
}

sync.Map vs Map with lock

sync 包也提供了并发安全的 map。不过,正如文档[6]中解释的,你应该谨慎的选择你使用的 map:

sync 包中的 map 类型是专业的。大多数代码应该使用原生的 Go map,附加上锁或者其他协调方式,这样类型安全更有保障,而且更容易维护其他的不变量和 map 的内容。

实际上,正如我的文章 “Go: 通过源码研究 Map 的设计[7]” 中所解释的,map 根据我们处理的具体类型提供了不同的方法。

让我们运行一个简单的基准测试,比较带有锁的常规 map 和 sync 包的 map。一个基准测试并发写入 map,另一个仅仅读取 map 中的值:

MapWithLockWithWriteOnlyInConcurrentEnc-868.2µs±2%
SyncMapWithWriteOnlyInConcurrentEnc-8192µs±2%
MapWithLockWithReadOnlyInConcurrentEnc-876.8µs±3%
SyncMapWithReadOnlyInConcurrentEnc-855.7µs±4%

我们可以看到,两种 map 各有千秋。我们可以根据具体的情况选择其中之一。文档[8]中很好地解释了这些情况:

map 类型针对两种常见使用场景做了优化:(1) 指定 key 的 entry 仅写入一次,但多次读取,比如只增长的缓存;(2) 多个 Goroutine 读取、写入、覆盖不相交的 key 的集合指向的 entry。

Map vs sync.Map

FAQ[9] 中也解释了他们做出了默认情况下 map 非并发安全这个决定的原因:

因此,要求所有的 map 操作都获取互斥锁,会拖慢大多数程序,但只为很少的程序增加了安全性

让我们运行一个不使用并发 Goroutine 的基准测试,来理解当你不需要并发但标准库默认提供并发安全的 map 时,可能带来的影响:

MapWithWriteOnly-811.1ns±3%
SyncMapWithWriteOnly-8121ns±6%

MapWithReadOnly-84.87ns±7%
SyncMapWithReadOnly-829.2ns±4%

简单的 map 快 7 到 10 倍。显然,在非并发模式下,这听起来更合理,巨大的差异也清楚的解释了为什么默认非并发安全的 map 是更好的选择。如果你不需要处理并发状况,为什么要让程序运行的更慢呢?


via: https://medium.com/@blanchon.vincent/go-concurrency-access-with-maps-part-iii-8c0a0e4eb27e

作者:blanchon.vincent[10]译者:DoubleLuck[11]校对:dingdingzhou[12]

本文由 GCTT[13] 原创编译,Go 中文网[14] 荣誉推出,发布在 Go语言中文网gongzhong号,转载请联系我们授权。

参考资料

[1]

Go: 通过源码研究 Map 的设计: https://studygolang.com/articles/22777

[2]

Go blog: https://blog.golang.org/go-maps-in-action

[3]

map 是非并发安全的: https://golang.org/doc/faq#atomic_maps

[4]

FAQ: https://golang.org/doc/faq#atomic_maps

[5]

数据争用检测器: https://medium.com/@blanchon.vincent/go-race-detector-with-threadsanitizer-8e497f9e42db

[6]

文档: https://golang.org/pkg/sync/

[7]

Go: 通过源码研究 Map 的设计: https://studygolang.com/articles/22777

[8]

文档: https://golang.org/pkg/sync/#Map

[9]

FAQ: https://golang.org/doc/faq#atomic_maps

[10]

blanchon.vincent: https://medium.com/@blanchon.vincent

[11]

DoubleLuck: https://github.com/DoubleLuck

[12]

dingdingzhou: https://github.com/dingdingzhou

[13]

GCTT: https://github.com/studygolang/GCTT

[14]

Go 中文网: https://studygolang.com/



推荐阅读

  • Go: 通过代码学习 Map 的设计 — Part II


我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。gongzhong号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

相关资源