Golang 并发地使用 range 访问 map。

huangapple go评论84阅读模式
英文:

Golang concurrent map access with range

问题

我有一个包含对象的地图,在清除地图之前需要释放这些对象。我想遍历地图,并在遍历过程中删除/释放对象。

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

由于唯一的遍历地图的方式是使用range,我该如何同步多个生产者和多个消费者?

我不想对地图进行读取锁定,因为这会导致在遍历过程中无法删除/修改键。

英文:

I have a map with objects that needs to be released before clearing the map. I am tempted to iterate over the map and remove/release objects as I walk through it.

Here is a mock up example
https://play.golang.org/p/kAtPoUgMsq

Since the only way to iterate the map is through range, how would I synchronize multiple producers and multiple consumers?

I don't want to read lock the map since that would make delete/modifying keys during the iteration impossible.

答案1

得分: 7

有很多方法可以在不产生竞争访问的情况下清理map中的内容。哪种方法适用于你的应用程序,很大程度上取决于它的功能。

  1. 在处理map时只需锁定它。如果map不太大,或者你可以容忍一些延迟,这样可以快速完成工作(从你的时间角度来看),然后你可以继续思考其他事情。如果以后出现问题,可以再回头解决。

  2. 在持有锁的同时,将对象或指针复制出来并清空map,然后在后台释放对象。如果问题在于释放的速度很慢会导致锁被长时间持有,这是一个简单的解决方法。

  3. 如果高效的读取是唯一重要的因素,可以使用atomic.Value。这允许你完全用一个新的不同的map替换原来的map。如果写入操作在你的工作负载中几乎为0%,高效的读取可以抵消每次更改时创建新map的成本。这种情况很少见,但确实存在,例如encoding/gob中就有一个全局的类型map就是通过这种方式管理的。

  4. 如果以上方法都不能满足你的需求,可以调整数据的存储方式(例如将map分片)。用16个map替换原来的map,然后自己对键进行哈希,决定将一个元素放入哪个map中,这样你就可以一次只锁定一个分片,进行清理或任何其他写操作。

还有一个问题是释放和使用之间的竞争:goroutine A从map中获取某个值,goroutine B清空map并释放该值,然后goroutine A使用了已释放的值。

一种策略是在使用或释放值时锁定每个值;这样你就需要锁,但不需要全局锁。

另一种策略是容忍竞争的后果,如果已知竞争不会造成灾难性后果的话;例如,net.Conn的文档明确允许并发访问,因此关闭正在使用的连接可能会导致对其的请求出错,但不会导致未定义的应用行为。不过,你必须确信自己清楚竞争的后果,因为许多看似无害的竞争实际上并非如此

最后,也许你的应用程序已经确保不会释放正在使用的对象,例如对象上有一个安全维护的引用计数,只有未使用的对象才会被释放。那么,你就不必担心了。

也许你会尝试用通道来替代这些锁,但我并没有看到任何好处。当你能够主要以进程间通信为主来设计应用程序时,这是很好的。但是当你确实有共享数据时,假装没有共享数据是没有意义的。锁的作用就是排除对共享数据的不安全访问。

英文:

There are a bunch of ways you can clean up things from a map without racy map accesses. What works for your application depends a lot on what it's doing.

  1. Just lock the map while you work on it. If the map's not too big, or you have some latency tolerance, it gets the job done quickly (in terms of time you spend on it) and you can move on to thinking about other stuff. If it becomes a problem later, you can come back to the problem then.

  2. Copy the objects or pointers out and clear the map while holding a lock, then release the objects in the background. If the problem is that the slowness of releasing itself will keep the lock held a long time, this is the simple workaround for that.

  3. If efficient reads are basically all that matters, use atomic.Value. That lets you entirely replace one map with a new and different one. If writes are essentially 0% of your workload, the efficient reads balance out the cost of creating a new map on every change. That's rare, but it happens, e.g., encoding/gob has a global map of types managed this way.

  4. If none of those do everything you need, tweak how you store the data (e.g. shard the map). Replace your map with 16 maps and hash keys yourself to decide which map a thing belongs in, and then you can lock one shard at a time, for cleanup or any other write.

There's also the issue of a race between release and use: goroutine A gets something from the map, B clears the map and releases the thing, A uses the released thing.

One strategy there is to lock each value while you use or release it; then you need locks but not global ones.

Another is to tolerate the consequences of races if they're known and not disastrous; for example, concurrent access to net.Conns is explicitly allowed by its docs, so closing an in-use connection may cause a request on it to error but won't lead to undefined app behavior. You have to really be sure you know what you're getting into then, though, 'cause many benign-seeming races aren't.

Finally, maybe your application already is ensuring no in-use objects are released, e.g. there's a safely maintained reference count on objects and only unused objects are released. Then, of course, you don't have to worry.

It may be tempting to try to replace these locks with channels somehow but I don't see any gains from it. It's nice when you can design your app thinking mainly in terms of communication between processes rather than shared data, but when you do have shared data, there's no use in pretending otherwise. Excluding unsafe access to shared data is what locks are for.

答案2

得分: 4

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

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

请注意,这是一段Go语言代码,用于从映射(map)中删除指定的键,并为每个被删除的键值对启动一个并发的释放操作。

英文:

You do not state all the requirements (e.g. can the release of multiple objects happen simultaneously, etc) but the simplest solution I can think of is to remove elements and launch a release goroutine for each of the removed elements:

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

答案3

得分: 2

更新于2017年8月(golang 1.9

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


原始答案于2016年11月

> 我不想对映射进行读取锁定

这是有道理的,因为从映射中删除被认为是写操作,必须与所有其他读取和写入操作进行串行化。这意味着需要写锁来完成删除操作。(来源:这个答案

假设最坏的情况(多个写入者和读取者),你可以查看orcaman/concurrent-map的实现,它使用多个sync.RWMutex来实现Remove()方法,因为为了避免锁瓶颈,这个并发映射被分成了几个(SHARD_COUNT)映射分片。这比只使用一个RWMutex在这个示例中要快。

英文:

Update August 2017 (golang 1.9)

You now have a new Map type in the sync package is a concurrent map with amortized-constant-time loads, stores, and deletes.
It is safe for multiple goroutines to call a Map's methods concurrently.


Original answer Nov. 2016

> I don't want to read lock the map

That makes sense, since a deleting from a map is considered a write operation, and must be serialized with all other reads and writes. That implies a write lock to complete the delete. (source: this answer)

Assuming the worst case scenario (multiple writers and readers), you can take a look at the implementation of orcaman/concurrent-map, which has a Remove() method using multiple sync.RWMutex because, to avoid lock bottlenecks, this concurrent map is dived to several (SHARD_COUNT) map shards.
This is faster than using only one RWMutex as in this example.

huangapple
  • 本文由 发表于 2015年12月4日 12:03:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/34080734.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定