Go: Map设计(3)-并发访问

汪明军_3145 · · 340 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

【译文】原文地址
Go博客中关于map的文章表明:map在并发使用中是不安全的,当同时对map进行读写结果是不确定的。如果多个Goroutine需要并发的对map进行读写,需要使用某种同步机制来保证读写安全。
然而,正如FAG中解释的,Goole提供了一些帮助:为了帮助正确使用map,Go语言的一些实现包含特殊的检查,当一个map被并发执行修改不安全时,该检查会在运行时自动报错。

数据竞争检测

我们可以从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

竞争检测显示第二个goroutine正在读,然而另一个goroutine正在对相应的value进行写。如果想了解更多这方面的内容,可以阅读其他数据竞争文章。

并发写检测

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。


image.png

然而,该标志位将在操作结束时被重置:

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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:汪明军_3145

查看原文:Go: Map设计(3)-并发访问

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

340 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传