同时将地图条目读入频道

时间:2016-06-23 19:53:26

标签: go

我有一个场景,我需要超出(尽可能多)地图条目并将它们发送到一个频道。通道另一端的操作可能需要很长时间,并且同时访问地图(并由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)
    }
}

1 个答案:

答案 0 :(得分:1)

我看到3个选择:

0)快照不一致这就是您所拥有的:当您生成密钥时,您的地图会发生变化,因此您可以获得所需的内容。我不完全确定你的锁定是否正确。它看起来真的很可疑。当然,用比赛探测器进行广泛的测试。

1)“停止世界” - 您可以在生成所有密钥时阻止对地图的写入权限。生成密钥比处理项目快得多,并且将为您提供要处理的项目的完美一致快照。不幸的是,当您将密钥发送到协同例程时,这些密钥在您处理它时可能不存在。这听起来你很好。它确实需要存储所有密钥的副本,所以希望这没关系。

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

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

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

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

但是处理所有没有锁定的条目(因为一切都只是阅读)可能会更好/更简单,然后等待所有 goroutines完成,然后通过执行来消除B “B = make()”得到一张空地图。这将立即释放所有内存,并保存一些在删除后需要完成的记帐。

擦除地图(或删除了所有条目)后,您可以在另一种方式翻转布尔值时使用RW锁定,然后开始处理其他地图。

缺点是,如果您经常更新项目,最终将获得2份地图副本。如果是这种情况:1)有WRITES检查后备地图。如果它在那里,请更新它,否则更新主地图。 2)在后台处理之前从地图中删除项目。 (你不能使用批量删除技巧。)