8

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

 4 years ago
source link: https://studygolang.com/articles/33035
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

汪明军_3145 · 大约7小时之前 · 25 次点击 · 预计阅读时间 3 分钟 · 不到1分钟之前 开始浏览    

【译文】原文地址
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。

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


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

280

本文来自:简书

感谢作者:汪明军_3145

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK