数据竞争检测

我们可以从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并发安全更好。如果您不需要处理并发性,为什么要使程序变慢呢?