Search code examples
goconcurrencysynchronization

Is sync.Map LoadOrStore subject to race conditions?


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?


Solution

  • 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.