Golang映射对并发读/写操作的安全性如何?

时间:2016-03-22 23:35:36

标签: go concurrency hashmap

根据Go博客,

  

映射对于并发使用是不安全的:它没有定义当您同时读取和写入时会发生什么。如果您需要从同时执行的goroutine中读取和写入映射,则访问必须由某种同步机制调解。   (来源:https://blog.golang.org/go-maps-in-action

有人可以详细说明吗?并发读取操作在例程中似乎是允许的,但是如果尝试读取和写入相同的密钥,则并发读/写操作可能会生成竞争条件。

在某些情况下可以降低最后的风险吗?例如:

  • 函数A生成k并设置m [k] = 0。这是A写入映射m的唯一时间。已知k不在m。
  • A将k传递给同时运行的功能B
  • 然后读取m [k]。如果m [k] == 0,则它等待,仅在m [k]!= 0
  • 时继续
  • B在地图中查找k。如果找到它,B将m [k]设置为某个正整数。如果不是,则等到k为m。

这不是代码(显然),但我认为它显示了一个案例的大纲,即使A和B都试图访问m,也不会有竞争条件,或者如果有的话无关紧要因为有额外的限制。

7 个答案:

答案 0 :(得分:36)

在Golang 1.6之前,并发读取没问题,并发写入不行,但写入和并发读取都可以。从Golang 1.6开始,映射在写入时无法读取。 所以在Golang 1.6之后,并发访问映射应该是:

package main

import (
    "sync"
    "time"
)

var m = map[string]int{"a": 1}
var lock = sync.RWMutex{}

func main() {
    go Read()
    time.Sleep(1 * time.Second)
    go Write()
    time.Sleep(1 * time.Minute)
}

func Read() {
    for {
        read()
    }
}

func Write() {
    for {
        write()
    }
}

func read() {
    lock.RLock()
    defer lock.RUnlock()
    _ = m["a"]
}

func write() {
    lock.Lock()
    defer lock.Unlock()
    m["b"] = 2
}

或者您将收到以下错误: enter image description here

<强>增加:

您可以使用go run -race race.go

检测种族

更改read功能:

func read() {
    // lock.RLock()
    // defer lock.RUnlock()
    _ = m["a"]
}

enter image description here

另一个选择:

众所周知,map由存储桶实现,sync.RWMutex将锁定所有存储桶。 concurrent-map使用fnv32分隔密钥,每个分支使用一个sync.RWMutex

答案 1 :(得分:10)

并发读取(只读)没问题。并发写和/或读不行。

如果访问是同步的,则多个goroutine只能写入和/或读取相同的地图,例如通过sync包,通道或其他方式。

你的例子:

  
      
  1. 函数A生成k并设置m [k] = 0。这是A写入映射m的唯一时间。已知k不在m。
  2.   
  3. A将k传递给同时运行的功能B
  4.   
  5. 然后读取m [k]。如果m [k] == 0,则它等待,仅在m [k]!= 0
  6. 时继续   
  7. B在地图中查找k。如果找到它,B将m [k]设置为某个正整数。如果不是,则等到k为m。
  8.   

您的示例有2个goroutines:A和B,A尝试读取m(在步骤3中),B尝试同时写入它(在步骤4中)。没有同步(你没有提及任何),所以不允许/不确定。

这是什么意思?未确定意味着即使B写m,A也可能永远不会观察到变化。或者A可能会观察到甚至没有发生的变化。或者可能发生恐慌。或者由于这种非同步的并发访问,地球可能会爆炸(尽管后一种情况的可能性极小,甚至可能低于1e-40)。

相关问题:

Map with concurrent access

what does not being thread safe means about maps in Go?

What is the danger of neglecting goroutine/thread-safety when using a map in Go?

答案 2 :(得分:6)

  

Go 1.6 Release Notes

     

运行时添加了轻量级,尽力而为的并发检测   滥用地图。和往常一样,如果一个goroutine正在写一张地图,没有   其他goroutine应该同时读取或写入地图。如果   运行时检测到这种情况,它会打印诊断并崩溃   该程序。了解问题的最佳方法是运行   比赛探测器下的程序,将更可靠地识别   比赛并提供更多细节。

地图是复杂的,自我重组的数据结构。并发读写访问是未定义的。

没有代码,没有什么可说的。

答案 3 :(得分:2)

您可以使用sync.Map,它可以安全地并发使用。唯一的警告是,您将放弃类型安全性,并更改对地图的所有读写操作,以使用为此类型定义的方法

答案 4 :(得分:1)

正如其他答案所述,原生map类型不是goroutine - 安全。阅读当前答案后的几个笔记:

  1. 不要使用延迟解锁,它有一些影响性能的开销(参见this好帖子)。直接致电解锁。
  2. 您可以通过减少锁之间的时间来获得更好的性能。例如,通过分割地图。
  3. 有一个共同的包(在GitHub上接近400颗星)用来解决这个问题concurrent-map here,它具有性能和可用性。您可以使用它来为您处理并发问题。

答案 5 :(得分:0)

您可以在地图中存储指向int的指针,并且有多个goroutine读取指向的int,而另一个goroutine将新值写入int。在这种情况下,地图没有更新。

这不是Go的习惯,而不是你所要求的。

或者不是将键传递给地图,而是可以将索引传递给数组,并将其更新为一个goroutine,而其他人则读取该位置。

但您可能只是想知道为什么当密钥已经在地图中时,地图的价值无法使用新值进行更新。据推测,地图的散列方案没有任何改变 - 至少没有给出它们当前的实现。 Go的作者似乎不想为这些特殊情况提供补贴。一般来说,他们希望代码易于阅读和理解,并且当其他goroutine可以读取时不允许地图写入的规则使事情变得简单,现在在1.6中他们甚至可以开始在正常运行时捕获误用 - 节省了许多人的数小时调试。

答案 6 :(得分:0)

经过长时间的讨论,人们决定对地图的典型使用不需要从多个goroutine安全访问,在那些情况下,地图可能是某些已经同步的较大数据结构或计算的一部分。因此,要求所有映射操作都获取互斥量将减慢大多数程序的速度,并增加少数程序的安全性。但是,这并不是一个容易的决定,因为这意味着不受控制的地图访问可能会使程序崩溃。

该语言不排除原子图更新。在需要时,例如在托管不受信任的程序时,该实现可以互锁地图访问。

仅当正在进行更新时,地图访问才是不安全的。只要所有goroutine都仅读取(在地图中查找元素,包括使用for范围循环对其进行遍历),并且不通过分配元素或执行删除操作来更改地图,则它们可以安全地并发访问地图而无需同步。

为更正地图的使用,该语言的某些实现包含特殊检查,当并发执行不安全地修改地图时,该检查会在运行时自动报告。