【问题标题】:Concurrently read map entries into a channel同时将映射条目读入通道
【发布时间】:2017-02-19 14:50:41
【问题描述】:

我有一个场景,我需要覆盖(尽可能多的)地图条目并将它们发送到通道中。通道另一端的操作可能需要很长时间,并且地图是同时访问的(并受RWMutex 保护)。地图也相当大,我想避免创建它的临时副本。

假设我有一个这样的结构:

type Example struct {
    sync.RWMutex
    m map[string]struct{}
}

现在我想出了这样的东西:

func (e *Example) StreamAll() <-chan string {
    toReturn := make(chan string)
    go func() {
        e.RLock()
        defer e.RUnlock()
        for k := range e.m {
            e.RUnlock()
            toReturn <- k
            e.RLock()
        }
        close(toReturn)
    }()
    return toReturn
}

language specification 有一个关于地图测距的有趣之处:

如果在迭代过程中删除了尚未到达的映射条目,则不会产生相应的迭代值。如果在迭代过程中创建了映射条目,则该条目可能会在迭代过程中生成,也可能会被跳过。

现在,我想知道的是:即使地图在迭代之间发生变化,是否可以保证我在地图上测距的方法有效?包括我上次读取的密钥被删除的情况?我不需要所有地图条目,但大部分都需要。

这是一个完整的例子:

package main

import (
    "fmt"
    "sync"
)

type Example struct {
    sync.RWMutex
    m map[string]struct{}
}

func NewExample() *Example {
    return &Example{
        m: make(map[string]struct{}),
    }
}

func (e *Example) Put(s string) {
    e.Lock()
    defer e.Unlock()
    e.m[s] = struct{}{}
}

func (e *Example) Delete(s string) {
    e.Lock()
    defer e.Unlock()
    delete(e.m, s)
}

func (e *Example) StreamAll() <-chan string {
    toReturn := make(chan string)
    go func() {
        e.RLock()
        defer e.RUnlock()
        for k := range e.m {
            e.RUnlock()
            toReturn <- k
            e.RLock()
        }
        close(toReturn)
    }()
    return toReturn
}

func main() {
    e := NewExample()
    e.Put("a")
    e.Put("b")

    values := e.StreamAll()
    // Assume other goroutines concurrently call Put and Delete on e
    for k := range values {
        fmt.Println(k)
    }
}

【问题讨论】:

  • 您到操场的链接不起作用。
  • 谢谢,我现在把它放到问题中。
  • range 操作本身应该以并发安全的方式执行,如果您的地图正在更改,您可能会得到奇怪的元素序列(每次您在地图上进行范围时,你得到的元素顺序会有所不同。你会从地图中删除元素吗?
  • 是的,还有其他 goroutine 在 Example 类型上调用 PutDelete。请注意,这两种方法都使用 Writer-Lock。我的逻辑是,只要 for-range 循环的 range 部分受到 Reader-Lock 的保护,我就不会并发读写。
  • 我想知道这怎么会突然引起这么多关注。我记得在没有得到关于 SO 的答案后,我将它交叉发布到 golang-nuts,这是线程:groups.google.com/forum/#!topic/Golang-nuts/vkysJuKen1A

标签: go


【解决方案1】:

我看到 3 个选择:

0) 不一致的快照 这就是您所拥有的:您的地图会随着您生成密钥而发生变化,因此您会得到所得到的。我不完全确定您的锁定是否正确。在我看来真的很可疑。当然,使用竞赛检测器对其进行广泛测试。

1) “Stop The World” - 您可以在生成所有密钥时阻止对地图的写访问。生成密钥比处理项目快得多,并且将为您提供要处理的项目的完美一致快照。不幸的是,当您将密钥发送到协同例程时,这些密钥可能在您开始处理它时不存在。听起来你可以接受。它确实需要存储所有密钥的副本,所以希望这没问题。

2) 滚动你自己的 MVCC(多版本并发控制) - 不使用 1 个映射,而是使用 2 个。我们称它们为 A 和 B。这个想法是只写入第一个映射在处理第二张地图时,然后翻转角色。

  • 在您的 RW Lock 旁边,添加一个布尔值以保护您的地图。
  • 照常取出您的 RW 锁,然后查看两张地图。
  • 当布尔值为真时,从 A 读取/写入,但如果在 A 中找不到读取,则允许“回退”到 B。
  • 当布尔值为 false 时,从 B 读取/写入,但如果在 B 中找不到读取,则允许“回退”到 A。

开始后台作业时,只需取出 RW 锁,同时翻转布尔值。现在您可以遍历“回退”映射的键,为每个键调用一个 gouroutine。密钥保证存在,因为没有人写入该映射。

您可以在 goroutine 中从 B 中删除(读取时需要使用锁定,删除时需要再次锁定)。

但是只处理所有没有锁定的条目可能会更好/更简单(因为一切都只是在读取),然后等待 所有 goroutine 完成,然后通过执行清除 B "B = make()" 得到一个空地图。这将立即释放您的所有内存,并节省一些需要在删除后进行的记帐。

一旦您擦除了地图(或删除了所有条目),您可以在将布尔值翻转为另一种方式时获取 RW 锁,然后开始处理另一个地图。

缺点是,如果您经常更新项目,您最终会得到 2 个地图副本。如果是这种情况:1) 让 WRITES 检查回退映射。如果它在那里,更新它,否则更新主地图。 2) 在后台处理之前从地图中删除该项目。 (您不能使用批量删除技巧。)

【讨论】:

  • 感谢您的详细解答!正如在 OP 下评论的那样,我确实在一段时间后将其发布到 golang-nuts,这是线程:groups.google.com/forum/#!topic/Golang-nuts/vkysJuKen1A。他们的观点与您的观点相似,实际上我可能不得不做一些类似 Erlang 解决方案或您在 2) 中描述的事情。目前,该项目尚未进行,尽管我仍然想知道我使用的锁定是否真的有效......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-05
  • 2013-01-12
  • 2022-12-10
  • 2019-06-16
  • 2010-11-06
  • 1970-01-01
  • 2012-06-29
相关资源
最近更新 更多