Reputation: 97
the following code will result in a data race detection when run with go run -race main.go
:
func main() {
var values *map[string]string
go func() {
for {
// First completely initialize the map
newMap := make(map[string]string)
newMap["foo"] = "bar"
// Write to shared pointer variable
values = &newMap
time.Sleep(time.Second)
}
}()
go func() {
for {
// Save pointer variable
savedMap := values
if savedMap == nil {
continue
}
// Do something with the hopefully safe map!
fmt.Println("savedMap:", savedMap)
time.Sleep(time.Second)
}
}()
}
Snippet of output:
==================
WARNING: DATA RACE
Write at 0x00c000120f10 by goroutine 28:
main.main.func1()
My question is: Is it really unsafe? Could I really end up with a mangled pointer address (one part of the new one and one part of the old one)? Or is this just a "best-practice" kind of thing where doing this might result in hard to debug problems later when additional code does not adhere to the "contract"?
In my use case I hardly ever update the pointer. 99.9% of the times this pointer will be read. So I hoped to not having to use synchronization features like atomic.Pointer/Mutexes and so on to safe on complexity and also surely performance (even though maybe minimally).
Upvotes: -1
Views: 90
Reputation: 240512
It's not (only) about how you're accessing the pointer. The pointer read is "relatively safe", because the Go memory model promises that values of machine word size or smaller can't have "torn reads" or read back a value that was never written. But there's nothing that guarantees that a different goroutine will see the result of the assignment to values
only after the initialization of newMap
! Guarantees are only provided in terms of "synchronizing operations" such as channel writes, mutex locks, or sync/atomic
accesses, by which all writes that "happen before" a synchronizing event in one goroutine are guaranteed to be visible to a synchronized-with goroutine.
The Go memory model doc specifically calls out a very similar example, in which one goroutine runs
func setup() {
a = "hello, world"
done = true
}
and another waits for done
to be true and then prints a
, however the doc notes
but there is no guarantee that, in
doprint
, observing the write todone
implies observing the write toa
. This version can (incorrectly) print an empty string instead of "hello, world".
Similarly, in your code, there is no guarantee that observing the write to values
implies observing the complete initialization of newMap
. Therefore the Println
could print an incorrect value, print an empty map, or simply crash.
Upvotes: 4
Reputation: 281748
This is unsafe. There is no guarantee that other goroutines will see a correctly initialized state of the map. A goroutine that sees the values = &newMap
write is not required to see all writes performed to initialize the map.
This can even lead to arbitrary memory corruption. Quoting the memory model docs:
Reads of memory locations larger than a single machine word are encouraged but not required to meet the same semantics as word-sized memory locations, observing a single allowed write w. For performance reasons, implementations may instead treat larger operations as a set of individual machine-word-sized operations in an unspecified order. This means that races on multiword data structures can lead to inconsistent values not corresponding to a single write. When the values depend on the consistency of internal (pointer, length) or (pointer, type) pairs, as can be the case for interface values, maps, slices, and strings in most Go implementations, such races can in turn lead to arbitrary memory corruption.
Upvotes: 3