数据竞争检测
我们可以从Go运行时得到的第一个帮助是数据竞争的检查。在运行go程序的时候使用-race参数,将会提供潜在的数据竞争提示。如下所示例子:
package main
import "sync"
func main() {
m := make(map[string]int, 1)
m[`foo`] = 1
var wg sync.WaitGroup
wg.Add(2)
go func() {
for i := 0; i < 1000; i++ {
m[`foo`]++
}
wg.Done()
}()
go func() {
for i := 0; i < 1000; i++ {
m[`foo`]++
}
wg.Done()
}()
wg.Wait()
}
在这个例子当中,我们明显地看到两个goroutine在某一时间对同一个值进行写。以下是竞争检测的输出内容:
==================
WARNING: DATA RACE
Read at 0x00c00008e000 by goroutine 6:
runtime.mapaccess1_faststr()
/usr/local/go/src/runtime/map_faststr.go:12 +0x0
main.main.func2()
main.go:19 +0x69
Previous write at 0x00c00008e000 by goroutine 5:
runtime.mapassign_faststr()
/usr/local/go/src/runtime/map_faststr.go:202 +0x0
main.main.func1()
main.go:14 +0xb8
并发写检测
Go还提供了一个并发写检测的功能。我们可以使用同一个例子,我们可以看到执行程序将打印如下错误:
fatal error: concurrent map writes
Go通过map结构体中的flags字段来管理并发。当程序试图修改map(赋新值、删除value或者清空map),flags字段的某一位会被设置为1:
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
[...]
h.flags ^= hashWriting
hashWriting的值是4,并将相应的位设置为1。 ^是一个异或操作,如果两个操作数的位相反,则将对应位设置为1。
然而,该标志位将在操作结束时被重置:
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
[...]
h.flags &^= hashWriting
}
现在已经为修改map的每个操作设置了控制,可以通过flags标志位来防止并发写。下面是flag的一个生命周期例子:
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
[...]
// if another process is currently writing, throw error
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
[...]
// no one is writing, we can set now the flag
h.flags ^= hashWriting
[...]
// flag reset
h.flags &^= hashWriting
sync.Map对比带锁的Map
Sync包提供了一个对并发使用安全的map。然而,正如文档所描述的,需要具体选择哪种更好需要根据情况来定:sync中map类型是一个定制化的,然而,大多数情况下我们只需要普通map并带独立锁或其他协同即可,这样能够更容易的维护map的其他的变量。
正如Go:map设计(2)所述,map提供函数是根据我们使用map类型来选择的。
我们可以运行一个基准测试:一个带锁的map和sync包中的map。一个基准测试将并发的写入值,另一个基准测试将只读map中的值:
MapWithLockWithWriteOnlyInConcurrentEnc-8 68.2µs ± 2%
SyncMapWithWriteOnlyInConcurrentEnc-8 192µs ± 2%
MapWithLockWithReadOnlyInConcurrentEnc-8 76.8µs ± 3%
SyncMapWithReadOnlyInConcurrentEnc-8 55.7µs ± 4%
正如我们看到的,两个map各有优势。根据情况,我们可以任意选择,这些情况在相关文档有说明:在读多写少的情况下使用sync.Map,在多并发写情况使用带锁map。
Map VS sync.Map
FAQ解释了为什么内建map不实现并发安全:需要所有的map操作都获取互斥锁的话会降低大多数程序的性能,而只为了少数的并发安全。
下面可以运行一个不需要带并发安全的map基准测试,来观察安全map对性能的影响:
MapWithWriteOnly-8 11.1ns ± 3%
SyncMapWithWriteOnly-8 121ns ± 6%
MapWithReadOnly-8 4.87ns ± 7%
SyncMapWithReadOnly-8 29.2ns ± 4%
发现简单map要快7到10倍。在非并发模式下,这听起来显然是合乎逻辑的,但巨大的差异明确解释了为什么不让默认map并发安全更好。如果您不需要处理并发性,为什么要使程序变慢呢?