我有一个结构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个选项
- 将映射
myMap
和MyStruct
的互斥mu
公开,并转移到客户端以确保循环线程的安全。这很简单,但不知何故,感觉MyStruct
并不太关心其客户 - 将所有内容保密,并添加一个方法,该方法将地图副本返回给希望安全地使用它的客户端;封装的观点,但同时,听起来有点沉重
还有其他可能吗?关于哪种设计更好,有什么建议吗?
有一个sync.Map
,它具有您需要的所有功能。主要的缺点是它不使用静态类型(由于Go中缺少泛型(。这意味着您必须在任何地方进行类型断言,才能像使用常规映射一样使用它。老实说,只使用sync.Map
并用静态类型重新声明所有方法可能是最简单的,这样客户端就不必担心进行类型断言。如果你不喜欢sync.Map
,请参阅我的其他建议。
首先要提到的一个改进是用sync.RWMutex
代替sync.Mutex
。这允许同时进行多个读取操作。然后,将Fetch
更改为使用mu.RLock()
和mu.RUnlock()
用于在地图中循环:
安全地迭代每个值并执行回调(在整个迭代中保持锁定(。注意,由于锁定,您不能在回调中调用Delete
或Add
,因此我们不能在迭代过程中修改映射。在迭代过程中修改映射在其他方面都是有效的,请参阅以下答案了解它的工作原理。
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在我看来是最干净的用法,但需要注意的是,由于它在整个迭代中不会持续保持锁,这意味着任何迭代都可能受到其他并发修改的影响。例如,如果在映射有5个键的情况下开始迭代,同时其他一些代码正在删除这些键,则无法判断迭代是否会看到所有5个键。