I'm using sync.Map
's LoadOrStore
method in Go. I'm trying to understand if there could be a race condition leading to multiple evaluations of the value creation function. I'm trying to understand if this is accurate and how to handle it correctly.
package main
import (
"fmt"
"sync"
)
type HashIDMap struct {
m sync.Map
}
func (hm *HashIDMap) GetOrCreate(key, value string, createFunc func(string, string)) string {
actual, loaded := hm.m.LoadOrStore(key, value)
if !loaded {
// If the value was not loaded, it means we stored it and need to create the database entry
createFunc(key, value)
}
return actual.(string)
}
func createDatabaseEntry(key, value string) {
// Simulate database entry creation
fmt.Printf("Creating database entry for key: %s with value: %s\n", key, value)
}
func main() {
hm := &HashIDMap{}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
id := hm.GetOrCreate("hash_1", "id_1", createDatabaseEntry)
fmt.Println("Goroutine 1 got ID:", id)
}()
go func() {
defer wg.Done()
id := hm.GetOrCreate("hash_1", "id_1", createDatabaseEntry)
fmt.Println("Goroutine 2 got ID:", id)
}()
wg.Wait()
}
I've read suggestions that createFunc
could actually be executed more than once due to a race condition.
Per my understanding, this is exactly what LoadOrStore
is trying to solve, but now I am not confident anymore whether a race condition is really possible.
Is is possible for createDatabaseEntry
to be called twice for same key?
It does not produce a race condition. The suggestion you read is wrong. The documentation of sync.Map
states (emphasis mine):
In the terminology of the Go memory model, Map arranges that a write operation “synchronizes before” any read operation that observes the effect of the write, where read and write operations are defined as follows. Load, LoadAndDelete, LoadOrStore, Swap, CompareAndSwap, and CompareAndDelete are read operations; Delete, LoadAndDelete, Store, and Swap are write operations; LoadOrStore is a write operation when it returns loaded set to false
Therefore, as it's reasonable to expect, a call to LoadOrStore
that returns false
(write op) is guaranteed to happen before a LoadOrStore
that returns true
(read op) for the same key.
This means that only one of the goroutines that concurrently call LoadOrStore
with the same key will see loaded=false
and call createDatabaseEntry
.