【问题标题】:How safe are Golang maps for concurrent Read/Write operations?并发读/写操作的 Golang 映射有多安全?
【发布时间】:2016-07-10 02:18:48
【问题描述】:

根据 Go 博客,

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

谁能详细说明一下?跨例程似乎允许并发读取操作,但如果尝试读取和写入同一个键,并发读取/写入操作可能会产生竞争条件。

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

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

这不是代码(显然),但我认为它显示了即使 A 和 B 都尝试访问 m 也不会出现竞争条件,或者如果存在则无关紧要的情况的轮廓因为额外的限制。

【问题讨论】:

标签: go concurrency hashmap


【解决方案1】:

在Golang 1.6之前,并发读是可以的,并发写是不行的,但是写并发读是可以的。从 Golang 1.6 开始,map 在写入时无法读取。 所以在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
}

否则您将收到以下错误:

添加:

您可以使用go run -race race.go 检测比赛

更改read函数:

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

另一个选择:

众所周知,map 是由存储桶实现的,sync.RWMutex 将锁定所有存储桶。 concurrent-map 使用fnv32 对密钥进行分片,每个桶使用一个sync.RWMutex

【讨论】:

  • 即使在 1.6 之前,并发读写也从来没有问题,它只是没有抱怨。
  • 这是一个非常好的解决方案。但是是否可以使用通道而不是互斥锁?
  • @newguy 我认为频道做不到。由于map在写入时无法读取,所以只使用一个通道来处理map
  • 我是否必须像这样锁定比较 -> if m["a"] == 1{} ?
  • > Golang 1.6之前,并发读可以,并发写不行,但是写并发读可以。这是完全不正确的。同时读取和写入地图一直是不正确的。
【解决方案2】:

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

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

你的例子:

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

您的示例有 2 个 goroutine: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?

【讨论】:

    【解决方案3】:

    Go 1.6 Release Notes

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

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

    没有代码,就没什么好说的了。

    【讨论】:

      【解决方案4】:

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

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

      只有在发生更新时,地图访问才是不安全的。只要所有的 goroutine 都只是读取——在 map 中查找元素,包括使用 for range 循环遍历它——而不是通过分配给元素或执行删除来更改 map,它们就可以安全地同时访问 map,而无需同步。

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

      【讨论】:

        【解决方案5】:

        您可以使用sync.Map,这对于并发使用是安全的。唯一需要注意的是,您将放弃类型安全并更改对映射的所有读写操作以使用为该类型定义的方法

        【讨论】:

        • 请注意,sync.Map 仅适用于锁定争用最终成为瓶颈的特定用例。对于大多数常见情况,建议使用带有互斥锁的本机映射:“映射类型是专门的。大多数代码应该使用普通的 Go 映射,具有单独的锁定或协调,以获得更好的类型安全性并更容易维护其他不变量连同地图内容。” golang.org/pkg/sync/#Map
        【解决方案6】:

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

        这对于 Go 来说不是惯用的,也不是你所要求的。

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

        但是您可能只是想知道为什么当键已经在映射中时不能用新值更新映射的值。据推测,地图的散列方案没有任何改变 - 至少没有考虑到它们当前的实现。 Go 的作者似乎不想考虑这种特殊情况。一般来说,他们希望代码易于阅读和理解,并且像在其他 goroutine 可以读取时不允许 map 写入这样的规则使事情变得简单,现在在 1.6 中,他们甚至可以开始在正常运行时发现误用 - 为许多人节省了很多小时调试。

        【讨论】:

        • 已经好几年了,我看到了更多关于比赛条件的讨论。我不喜欢三年前的回答,因为即使读取和写入 int,就像写入由另一个变量指向的 int 一样,被知道的人认为是不安全的。它会起作用吗?在某些架构上,可能。但是大多数处理器硬件工程师仍然会说结果是不确定的。 golang sync/atomic 包是我们的朋友,它可以从 golang 支持的各种架构中执行必要的操作。
        【解决方案7】:

        正如这里的其他答案所述,本机 map 类型不是 goroutine-safe。阅读当前答案后的几点说明:

        1. 不要使用 defer 解锁,它有一些影响性能的开销(请参阅this 好帖子)。直接调用解锁。
        2. 您可以通过减少锁之间花费的时间来获得更好的性能。例如,通过分片地图。
        3. 有一个通用包(在 GitHub 上接近 400 颗星)用于解决此问题,称为 concurrent-map here,它考虑了性能和可用性。您可以使用它来为您处理并发问题。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-07-08
          • 1970-01-01
          • 2017-07-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多