【问题标题】:Golang concurrent map access with range带范围的 Golang 并发地图访问
【发布时间】:2016-03-08 22:29:11
【问题描述】:

我有一张地图,其中包含需要在清除地图之前释放的对象。我很想在遍历地图时迭代地图并删除/释放对象。

这是一个模拟示例 https://play.golang.org/p/kAtPoUgMsq

由于迭代地图的唯一方法是通过范围,我将如何同步多个生产者和多个消费者?

我不想读取锁定地图,因为这会使迭代期间无法删除/修改键。

【问题讨论】:

  • 我们需要更多的上下文来处理这个问题。地图包含什么?它很大吗?发布一个东西有多快? (什么正在释放?)如果一个释放的对象被另一个goroutine使用,这是否是一场灾难?如果是,是什么决定了从地图中挑选出来的对象还能使用多久?什么是应用程序以及它的优先级是什么(例如最大化吞吐量与限制延迟)?

标签: data-structures go concurrency


【解决方案1】:

您没有说明所有要求(例如,是否可以同时释放多个对象等),但我能想到的最简单的解决方案是删除元素并为每个删除的元素启动一个发布 goroutine:

for key := range keysToRemove {
    if v, ok := m[k]; ok {
        delete(m, k)
        go release(k, v)
    }
}

【讨论】:

  • 这个答案中给出的方法真的够用吗? (特别是在 1.6 更改之后?)在语言规范中是否有一个地方可以清除/明确回答?
  • 在上面的代码示例中,map 是从单个 goroutine 访问的,因此不需要同步,因为没有并发访问。
  • 当然 OP 假设在迭代期间会有并发的编写者。他说:“我不想读取锁定地图,因为这会使迭代期间无法删除/修改键。”仅仅因为他不/显示/作家并不意味着他们没有参与。
  • 我不太确定问题是什么。如果你问在不同步的情况下从多个 goroutine 读取/写入 map 是否安全,那么答案是否定的。但是,有时可以重新编写代码以使访问成为单线程。这就是我试图通过将 delete(m, k) 移出单独的 goroutine 来在我的答案中展示的内容。很难说这个答案是否符合 OP 要求,因为问题中没有足够的要求。
【解决方案2】:

2017 年 8 月更新 (golang 1.9)

您现在在 sync 包中有一个新的 Map 类型,它是一个具有分摊常数时间加载、存储和删除的并发映射。
多个 goroutine 同时调用 Map 的方法是安全的。


2016 年 11 月的原始答案

我不想读锁地图

这是有道理的,因为从映射中删除被视为写入操作,并且必须与所有其他读取和写入一起序列化。这意味着一个写锁来完成删除。 (来源:this answer

假设最坏的情况(多个写入器和读取器),你可以看看orcaman/concurrent-map的实现,它有一个Remove() method使用多个sync.RWMutex,因为为了避免锁瓶颈,这个并发映射是潜入了几个 (SHARD_COUNT) 地图碎片。
这比只使用一个RWMutex as in this example 更快。

【讨论】:

    【解决方案3】:

    您可以通过多种方式清理map 中的内容,而无需访问不雅的地图。对您的应用程序有效的方法很大程度上取决于它在做什么。

    0) 工作时只需锁定地图即可。如果地图不是太大,或者您有一定的延迟容忍度,它可以快速完成工作(就花费的时间而言),您可以继续考虑其他事情。如果以后变成问题,你可以再回到问题上来。

    1) 将对象或指针复制出来并在持有锁的同时清除地图,然后在后台释放对象。如果问题是释放本身的缓慢会使锁保持很长时间,这是解决此问题的简单方法。

    2) 如果高效读取基本上是最重要的,请使用atomic.Value。这使您可以用新的不同地图完全替换一张地图。如果写入基本上是您工作量的 0%,那么高效的读取可以平衡每次更改时创建新映射的成本。这种情况很少见,但会发生,例如,encoding/gob 有一个以这种方式管理的类型的全局映射。

    3) 如果这些都不能满足您的所有需求,请调整存储数据的方式(例如,对地图进行分片)。自己用 16 个映射和哈希键替换您的映射,以决定一个事物属于哪个映射,然后您可以一次锁定一个分片,以进行清理或任何其他写入。

    还有释放和使用之间的竞争问题:goroutine A 从地图中获取一些东西,B 清除地图并释放东西,A 使用释放的东西。

    一种策略是在您使用或释放​​每个值时锁定它;那么你需要锁而不是全局锁。

    另一个是容忍种族的后果,如果它们是已知的并且不是灾难性的;例如,其文档明确允许同时访问net.Conns,因此关闭正在使用的连接可能会导致对其的请求出错,但不会导致未定义的应用程序行为。不过,你必须真正确定你知道你要进入什么,因为many benign-seeming races aren't

    最后,也许您的应用程序已经确保不会释放正在使用的对象,例如对对象有一个安全维护的引用计数,并且只释放未使用的对象。那么,当然,您不必担心。

    尝试以某种方式用通道替换这些锁可能很诱人,但我看不到任何收益。 很好当您可以设计您的应用程序时主要考虑进程之间的通信而不是共享数据,但是当您确实有共享数据时,假装没有用处。排除对共享数据的不安全访问是锁的用途。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-02-06
      • 1970-01-01
      • 2018-09-04
      • 2016-11-06
      • 2018-05-30
      • 2021-11-23
      • 2016-07-22
      相关资源
      最近更新 更多