1

我有一个结构,MyStruct其中包含一个地图。我想让对地图的访问对并发读写安全,但我也想坚持基础Map而不是使用sync.Map.

出于这个原因,我创建了MyStruct受互斥锁保护的插入、删除和获取方法。代码看起来像这样

type MyStruct struct {
    mu    sync.Mutex
    myMap map[string]string
}

func (myStruct *MyStruct) Add(val string) {
    myStruct.mu.Lock()
    myStruct.myMap[val] = val
    myStruct.mu.Unlock()
}

func (myStruct *MyStruct) Remove(val string) {
    myStruct.mu.Lock()
    delete(myStruct.myMap, val)
    myStruct.mu.Unlock()
}

func (myStruct *MyStruct) Fetch(val string) string {
    myStruct.mu.Lock()
    ret := delete(myStruct.myMap, val)
    myStruct.mu.Unlock()
    return ret
}

到目前为止,一切都很好。

一些客户MyStruct虽然也需要循环,但myStruct.myMap我的问题来了。哪个是最好的设计来使并发安全的循环操作不在 MyStruct 的方法中执行?目前我看到 2 个选项

  1. 将地图myMap和互斥锁设为公共muMyStruct并将责任转移给客户端,以确保循环线程安全。这很简单,但不知何故,感觉好像MyStruct不太关心它的客户
  2. 保持一切私密,并添加一个方法,将地图副本返回给想要安全使用它的客户。从“封装”的角度来看,这似乎更好,但同时听起来有点沉重

还有其他可能吗?关于哪种设计更好的建议?

4

1 回答 1

2

sync.Map,它具有您需要的所有功能。主要缺点是它不使用静态类型(由于 Go 中缺少泛型)。这意味着您必须在任何地方都进行类型断言才能像使用常规映射一样使用它。老实说,使用静态类型并重新声明所有方法可能是最简单sync.Map的,这样客户就不必担心进行类型断言。如果您不喜欢sync.Map,请参阅我的其他建议。

首先要提到的一项改进是替换sync.Mutexsync.RWMutex. 这允许同时发生多个读取操作。然后,更改Fetch为使用mu.RLock()mu.RUnlock()

对于遍历地图

安全地迭代每个值并执行回调(为整个迭代保持锁定)。注意由于加锁,不能在回调中调用Deleteor Add,所以我们不能在迭代过程中修改map。否则在迭代期间修改地图是有效的,请参阅此答案以了解其工作原理。

func (myStruct *MyStruct) Range(f func(key, value string)) {
    myStruct.mu.RLock()
    for key, value := range myStruct.myMap {
        f(key, value)
    }
    myStruct.mu.RUnlock()
}

这是用法的样子

mystruct.Range(func(key, value string) {
    fmt.Println("map entry", key, "is", value)
})

这里也是一样的,只是用回调传入地图,这样回调函数就可以直接修改地图了。如果迭代进行修改,也会更改为常规锁。请注意,现在如果回调保留对地图的引用并将其存储在某处,它将有效地破坏您的封装。

func (myStruct *MyStruct) Range(f func(m map[string]string, key, value string)) {
    myStruct.mu.Lock()
    for key, value := range myStruct.myMap {
        f(myStruct.myMap, key, value)
    }
    myStruct.mu.Unlock()
}

这是一个使用更简洁的选项,因为锁定是经过精心管理的,因此您可以在回调中使用其他锁定函数。

func (myStruct *MyStruct) Range(f func(key, value string)) {
    myStruct.mu.RLock()
    for key, value := range myStruct.myMap {
        myStruct.mu.RUnlock()

        f(key, value)

        myStruct.mu.RLock()
    }
    myStruct.mu.RUnlock()
}

请注意,读锁总是在范围代码执行时被持有,但在f执行时永远不会被持有。这意味着测距是安全的*,但回调f可以自由调用任何其他Delete需要锁定的方法。

脚注:虽然选项#3 在我看来是最干净的用法,但主要需要注意的是,由于它不会在整个迭代中连续持有锁,这意味着任何迭代都可能受到其他并发修改的影响。例如,如果您在 map 有 5 个键时开始迭代,同时其他一些代码正在删除这些键,那么您不能说迭代是否会看到所有 5 个键。

于 2021-05-23T09:12:40.217 回答