Golang中针对特定键值加锁的场景:内置map搭配互斥锁与sync.Map的性能对比
Golang中针对特定键值加锁的场景:内置map搭配互斥锁与sync.Map的性能对比 我正在使用 map[float64][]custom_data。我的使用场景是:每个键对应的值只能由一个 Go 协程进行添加或删除操作,如果某个键的值([]custom_data)正在被一个 Go 协程使用,其他协程应该等待该操作完成。
据我所知,sync.Map 会锁定整个映射,但我不想锁定整个映射,而是希望锁定特定键的值或自定义数据数组。在这种情况下,我应该使用内置映射还是 sync.Map?
map[float64][]custom_data
感谢使用 使用 sync.RWMutex 运行正常,但有时会抛出映射读写失败的错误
更多关于Golang中针对特定键值加锁的场景:内置map搭配互斥锁与sync.Map的性能对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
感谢您提供的信息。
由于两个或多个 Go 协程可能同时操作同一个键的值,因此需要并发控制。我希望锁定值以避免混乱的操作。
我想了解的是,sync.Map 是锁定整个映射,还是仅锁定特定键的值?
我认为使用标准的同步库是无法实现的。互斥锁机制并不是以特定方式锁定你的变量,而是确保只有一个 goroutine 能够访问你的变量。
如果你仍然想要实现这个功能,可以基于信号量实现其他动态结构来替代映射,让每个结构体拥有自己的标志位,这样每次读写操作都可以检查该标志。
关于如何为你的映射实现并发安全,你比我们更清楚你的使用意图,因此最终决定权在你手中。
不过,这里有一篇文章详细阐述了你可用的多种选择:https://medium.com/[@deckarep](/user/deckarep)/the-new-kid-in-town-gos-sync-map-de24a6bf7c2c
但我很好奇你具体想实现什么功能,因为你可能会发现你的工作甚至不需要并发支持——特别是如果每次只有一个 Go 协程会访问它的话。不过,sync.Map 似乎正好能满足我认为你需要的功能。
我经常使用这种简单的锁机制
var m = &sync.Mutex{} // 甚至作为全局变量
// 在代码的某个地方
m.Lock()
some_name[key_name] = some_value
m.Unlock()
因此,以这种方式访问是安全的(至少我没有遇到过问题)。当然,你也可以使用读锁或写锁。如果出现错误(恐慌?),请确保是由于同步原因而不是其他相关原因造成的。
关于性能,即使在大型映射上,操作通常也很快完成。唯一可能出现的问题是当映射过大时无法及时更新,或者由于过多的锁导致无法及时读取,但这些属于大规模场景,可以通过其他特定方式处理,我认为不适合使用映射来解决。
针对某个键的值必须仅由一个 Go 协程进行添加或删除
这说明读取器/修改器协程是与键绑定的。
但是
如果某个键的值(自定义数据)正在被一个 Go 协程使用,则其他协程应等待其操作完成
这表明它们并非绑定关系,您只是希望避免并发访问导致的数据竞争。
首先使用 sync.RWMutex 来控制映射的访问。
如果性能不够理想,您可以将映射分片为多个子映射,每个子映射拥有独立的锁。
具体取决于您的使用场景,但或许“为每个键创建管理协程,通过通道传递命令(获取/设置/修改)”的模式也能适用。
对于你的场景,使用内置map配合细粒度锁是更合适的选择。sync.Map确实不适合需要针对特定键进行并发控制的场景,因为它提供的是对整个map的原子操作,而不是针对特定键的细粒度锁。
以下是使用内置map配合互斥锁的解决方案:
package main
import (
"sync"
)
type custom_data struct {
// 你的自定义数据结构
Value string
}
type KeyLockMap struct {
mu sync.RWMutex
locks map[float64]*sync.Mutex
data map[float64][]custom_data
}
func NewKeyLockMap() *KeyLockMap {
return &KeyLockMap{
locks: make(map[float64]*sync.Mutex),
data: make(map[float64][]custom_data),
}
}
// 获取指定键的锁
func (m *KeyLockMap) getLock(key float64) *sync.Mutex {
m.mu.Lock()
defer m.mu.Unlock()
if lock, exists := m.locks[key]; exists {
return lock
}
lock := &sync.Mutex{}
m.locks[key] = lock
return lock
}
// 添加数据到指定键
func (m *KeyLockMap) Add(key float64, value custom_data) {
lock := m.getLock(key)
lock.Lock()
defer lock.Unlock()
m.data[key] = append(m.data[key], value)
}
// 从指定键删除数据
func (m *KeyLockMap) Remove(key float64, index int) {
lock := m.getLock(key)
lock.Lock()
defer lock.Unlock()
if index >= 0 && index < len(m.data[key]) {
m.data[key] = append(m.data[key][:index], m.data[key][index+1:]...)
}
}
// 获取指定键的数据
func (m *KeyLockMap) Get(key float64) []custom_data {
lock := m.getLock(key)
lock.Lock()
defer lock.Unlock()
return m.data[key]
}
// 清理指定键的锁(当不再需要该键时调用)
func (m *KeyLockMap) CleanupLock(key float64) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.locks, key)
}
使用示例:
func main() {
keyLockMap := NewKeyLockMap()
var wg sync.WaitGroup
// 模拟多个协程操作不同键
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
key := float64(i % 3) // 3个不同的键
// 添加数据
keyLockMap.Add(key, custom_data{Value: "data"})
// 获取数据
data := keyLockMap.Get(key)
_ = data
// 删除第一个元素
if len(data) > 0 {
keyLockMap.Remove(key, 0)
}
}(i)
}
wg.Wait()
}
这个实现的特点:
- 细粒度锁:每个键有独立的互斥锁,只有操作相同键的协程会相互阻塞
- 线程安全:使用读写锁保护锁映射的并发访问
- 内存管理:提供锁清理机制,避免内存泄漏
- 高性能:不同键的操作可以完全并行执行
相比之下,sync.Map在这种场景下的性能会较差,因为:
// 使用sync.Map的示例 - 不推荐
var m sync.Map
// 每次操作都需要锁定整个map
func addData(key float64, value custom_data) {
actual, _ := m.LoadOrStore(key, &sync.Mutex{})
lock := actual.(*sync.Mutex)
lock.Lock()
defer lock.Unlock()
// 这里还需要额外的同步来操作slice
}
对于你的特定需求,内置map配合细粒度锁的方案在性能和功能上都优于sync.Map。


