gothread-safety

Is map assignment thread-safe in Go?


I already know that the map operation is not thread-safe. For example if there are several go routine to read and update one same map(as @rustyx refered, just as s := cache["foo"] and cache["foo"] = "bar" ), I should use the RWMutex or other sync operation to avoid the data race.

My confusion is whether the assignment is thread-safe. For an instance, there are several goroutines may assign a new map to a map variable(cache = newCache).

If I can accept that a map generated in any goroutine is assigned to the var finally, will no mutex assignment cause data race?

What indead the assignment do? Is it atomic? If it was an atomic opertion, is it still not thread-safe?

package main

import (
    "fmt"
    "sync"
)

var cache map[string]string = make(map[string]string)

func main() {

    for j := 0; j < 1000; j++ {
        var wg sync.WaitGroup
        // start multiple routines to read and assign
        for i := 0; i < 1000; i++ {
            wg.Add(1)
            go func(i int) {
                defer wg.Done()
                if i%2 == 0 {
                    newCache := map[string]string{
                        fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i),
                    }
                    cache = newCache
                } else {
                    newCache := cache
                    fmt.Println(newCache)
                }
            }(i)
        }
        wg.Wait()
    }
}

Solution

  • I beleive, to understand your problem it's better to dig a bit deeper.

    What you call "map assignment" is not it, but rather, it's merely assignment of a value to a variable. That's what cache = newCache does: the = operator reads the value of newCache and writes it to the variable cache.

    Now "map reads and updates" are a bit more tricky as they are two-step operations: in order for s := cache["foo"] and cache["foo"] = "bar" to work, the following needs to be done:

    1. The current value contained in the variable cache needs to be read.
    2. That value (a map, actually) is then operated on to perform read or update operation on it.

    As you can see, reads and writes of a map do access the variable in which the map value is contained, and so if this is done concurrently by multiple goroutines at least one of which is writing to that variable, it is a data race, so the goroutines must be serialized in their access.

    Hence basically the simple way to go is to perform any operation under (the same) lock.


    Is it atomic? If it was an atomic opertion, is it still not thread-safe?

    In Go, no operation is atomic unless it's performed using primitives from sync/atomic (most of which are compiler intrinsics). That is, yes, in the "de-facto standard" implementation of Go, a map value is currently a single pointer, and, say, on x86, reading and writing pointer-sized variables is atomic (I don't remember whether this requires memory addresses of such variables be naturally aligned, or not; Go guarantees proper alignment anyway), but doing that directly (I mean, not via something like atomic.Value) is still an error (which will be trapped by a race detector).

    Update: also please note — just in case, — that atomicity is a reasonably broad concept. In Go, atomics have eventually received an explicitly stated guarantee of being so-called sequentially consistent, but other programming lanugages (or usages of this term in other contexts) do not necessarily have the same guarantees. For instance, atomicity may only guarantee the absence of so-called torn reads or torn writes (when a variable is updated concurrently, readers always see either the state the variable had before update, or has after, but not half-before and half-after), but does not guarantee that an update made by one CPU/core is necessarily propagated to the caches of other CPUs/cores (and so they may use the old value for an indeterminate period of time).