Go中sync.Map的LoadOrStore是否存在竞态,会重复执行创建函数?
sync.Map的LoadOrStore后执行createFunc是否会因竞态条件多次调用?
结论
你的代码确实存在竞态条件,针对同一个key,createDatabaseEntry可能被多个goroutine重复执行。
原因分析
sync.Map.LoadOrStore本身是原子操作,能保证同一个key只会被存储一次值,但问题出在你把createFunc的调用放在了LoadOrStore之后的分支判断里:
- 当多个goroutine同时调用
GetOrCreate且目标key不存在时,它们都会通过LoadOrStore得到loaded=false的结果(原子操作只保证最终只有一个值被存储,但所有并发调用都会感知到之前没有值); - 这些goroutine都会进入
if !loaded的代码块,各自执行createFunc,导致函数被多次调用。
举个你的代码的执行流程:
- Goroutine 1和Goroutine 2同时执行
hm.m.LoadOrStore("hash_1", "id_1"),此时key不存在; - 两个goroutine都返回
(actual: "id_1", loaded: false); - 两者都调用
createDatabaseEntry("hash_1", "id_1"),最终输出两次创建日志。
修正方案
要避免createFunc被重复执行,需要把"创建值"和"存储值"的逻辑做原子性绑定,或者用同步机制确保创建逻辑只执行一次。
方案1:容忍createFunc重复执行但保证存储值唯一
如果createFunc是幂等的(重复执行不影响结果),或者执行成本很低,可以用这种方式:
func (hm *HashIDMap) GetOrCreate(key string, createFunc func() string) string { // 先尝试读取已有值 if actual, ok := hm.m.Load(key); ok { return actual.(string) } // 创建新值 newValue := createFunc() // 尝试存储,若此时已有其他goroutine存储了值,则返回已有值 actual, loaded := hm.m.LoadOrStore(key, newValue) if loaded { return actual.(string) } return newValue }
这种方式下,createFunc仍可能被多次执行,但最终存储到sync.Map里的只会是一个值,调用方拿到的结果是一致的。
方案2:确保createFunc只执行一次
如果createFunc执行成本很高(比如写入数据库),必须确保只执行一次,可以结合sync.Once和sync.Map:
type HashIDMap struct { m sync.Map oncePool sync.Map // 存储每个key对应的sync.Once } func (hm *HashIDMap) GetOrCreate(key string, createFunc func() string) string { // 先尝试读取已有值 if actual, ok := hm.m.Load(key); ok { return actual.(string) } // 获取当前key对应的sync.Once,不存在则创建 onceVal, _ := hm.oncePool.LoadOrStore(key, &sync.Once{}) once := onceVal.(*sync.Once) var result string once.Do(func() { // 仅执行一次创建逻辑 result = createFunc() // 将创建好的值存入主map hm.m.Store(key, result) }) // 再次读取确保拿到正确的值 actual, _ := hm.m.Load(key) return actual.(string) }
sync.Once.Do会保证传入的函数只被执行一次,无论多少goroutine并发调用,彻底避免createFunc重复执行。
总结
sync.Map.LoadOrStore只保证值的存储是原子性的,但不会同步后续的业务逻辑。你的原始代码中,多个goroutine在判断!loaded后会无同步地执行createFunc,这就是竞态条件的根源。通过调整创建逻辑的时机或引入sync.Once,可以解决这个问题。
内容的提问来源于stack exchange,提问作者Zanko
相关产品推荐
相关产品推荐

