在Go语言中封装“并发安全”的Map

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

Encapsulating "concurrency safety" in Go Maps

问题

我有一个结构体MyStruct,其中包含一个映射(map)。我希望使对映射的访问在并发读写时是安全的,但我也希望坚持使用基本的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方法之外也能并发安全的最佳设计?目前我看到两种选择:

  1. MyStruct的映射myMap和互斥锁mu设为公共的,并将使循环线程安全的责任移交给客户端。这种方法简单,但在某种程度上感觉MyStruct对其客户端不太关心。
  2. 保持所有内容私有,并添加一个方法,将映射的副本返回给希望安全操作的客户端。从"封装"的角度来看,这似乎更好,但同时也有点重。

还有其他可能性吗?对于哪种设计更好有什么建议吗?

英文:

I have a struct, MyStruct, which contains a map. I want to make the access to the map safe for concurrent read and write but I also want to stick to the base Map and not use sync.Map.

For this reason I create on MyStruct methods for insert, delete and fetch which are protected by a mutex. The code looks like this

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
}

So far so good.

Some clients of MyStruct though need also to loop through myStruct.myMap and here comes my question. Which is the best design to make concurrent safe also loop operations performed not in methods of MyStruct? Currently I see 2 options

  1. Make the map myMap and the mutex mu of MyStruct public and move to the clients the responsibility to make the loop thread safe. This is simple but, somehow, feels like MyStruct does not care too much about its clients
  2. Keep everything private and add a method that returns a copy of the map to clients which wants safely play with it. This seems better from an "encapsulation' point of view but, at the same time, sounds a bit heavy

Is there any other possibility? Any suggestion on which design is better?

答案1

得分: 2

有一个名为sync.Map的结构体,它具备你所需的所有功能。主要的缺点是它不使用静态类型(因为Go语言中没有泛型)。这意味着你必须在使用它时到处进行类型断言,就像使用普通的map一样。老实说,最简单的方法可能是直接使用sync.Map,并重新声明所有方法的静态类型,这样客户端就不必担心进行类型断言了。如果你不喜欢sync.Map,可以看看我提供的其他建议。

首先要提到的一个改进是将sync.Mutex替换为sync.RWMutex。这样可以允许多个读操作同时进行。然后,将Fetch改为使用mu.RLock()mu.RUnlock()

遍历map

安全地迭代每个值并执行回调函数(在整个迭代过程中保持锁定)。请注意,由于加锁的原因,在回调函数中不能调用DeleteAdd,因此我们不能在迭代过程中修改map。在迭代过程中修改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)
})

以下是相同的代码,但是将map与回调函数一起传递,以便回调函数可以直接修改map。同时,为了防止迭代过程中进行修改,将锁定方式更改为常规锁定。请注意,现在如果回调函数保留对map的引用并将其存储在某个地方,它将有效地破坏了封装性。

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()
}

请注意,读锁在range代码执行期间始终被持有,但在f执行期间不会被持有。这意味着遍历是安全的*,但回调函数f可以自由地调用任何其他需要加锁的方法,比如Delete

注:尽管我个人认为选项#3的用法最清晰,但主要要注意的是,由于它不会在整个迭代过程中持续持有锁定,这意味着任何迭代都可能受到其他并发修改的影响。例如,如果在map具有5个键的情况下开始迭代,并且与此同时其他代码正在删除这些键,那么无法确定迭代是否会看到所有5个键。

英文:

There is sync.Map, which has all the features you need. The main downside is that it doesn't use static typing (due to the lack of generics in Go). This means that you have to do type assertions everywhere to use it like a regular map. Honestly it may be simplest to just use sync.Map and redeclare all of the methods with static types so that clients don't have to worry about doing the type assertions. If you don't like sync.Map, see my other suggestions.

One improvement to mention first, is to replace sync.Mutex with sync.RWMutex. This allows for multiple read operations to happen concurrently. Then, change Fetch to use mu.RLock() and mu.RUnlock()

For looping through the map:

Safely iterate over each value and execute the callback (keeps lock for entire iteration). Note that due to the locking, you can't call Delete or Add in the callback, so we can't modify the map during iteration. Modifying a map during iteration is otherwise valid, see this answer for how it works.

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

Here's what the usage would look like

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

Here's the same, but passing in the map with the callback so that the callback function can modify the map directly. Also changing to regular lock in case iteration makes a modification. Note that now if the callback retains a reference to the map and stores it somewhere, it will effectively break your encapsulation.

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()
}

Here's an option with cleaner usage, as the locking is carefully managed so you can use your other locking functions in the callback.

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()
}

Notice that the read lock is always held while the range code executes, but is never held while f executes. This means that the ranging is safe*, but the callback f is free to call any other methods like Delete that require locking.

Footnote: While option #3 has the cleanest usage in my opinion, the main thing to note is that since it doesn't hold a lock continuously for the entire iteration, that means that any iteration can be affected by other concurrent modifications. For example, if you start iterating while the map has 5 keys, and concurrently to this some other code is deleting the keys, you can't say whether or not the iteration will see all 5 keys.

huangapple
  • 本文由 发表于 2021年5月23日 16:33:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/67657750.html
匿名

发表评论

匿名网友

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

确定