Is it safe to read a function pointer concurrently without a lock?

4.9k views Asked by At

Suppose I have this:

go func() {
    for range time.Tick(1 * time.Millisecond) {
        a, b = b, a
    }
}()

And elsewhere:

i := a // <-- Is this safe?

For this question, it's unimportant what the value of i is with respect to the original a or b. The only question is whether reading a is safe. That is, is it possible for a to be nil, partially assigned, invalid, undefined, ... anything other than a valid value?

I've tried to make it fail but so far it always succeeds (on my Mac).

I haven't been able to find anything specific beyond this quote in the The Go Memory Model doc:

Reads and writes of values larger than a single machine word behave as multiple machine-word-sized operations in an unspecified order.

Is this implying that a single machine word write is effectively atomic? And, if so, are function pointer writes in Go a single machine word operation?

Update: Here's a properly synchronized solution

3

There are 3 answers

11
icza On

Unsynchronized, concurrent access to any variable from multiple goroutines where at least one of them is a write is undefined behavior by The Go Memory Model.

Undefined means what it says: undefined. It may be that your program will work correctly, it may be it will work incorrectly. It may result in losing memory and type safety provided by the Go runtime (see example below). It may even crash your program. Or it may even cause the Earth to explode (probability of that is extremely small, maybe even less than 1e-40, but still...).

This undefined in your case means that yes, i may be nil, partially assigned, invalid, undefined, ... anything other than either a or b. This list is just a tiny subset of all the possible outcomes.

Stop thinking that some data races are (or may be) benign or unharmful. They can be the source of the worst things if left unattended.

Since your code writes to the variable a in one goroutine and reads it in another goroutine (which tries to assign its value to another variable i), it's a data race and as such it's not safe. It doesn't matter if in your tests it works "correctly". One could take your code as a starting point, extend / build on it and result in a catastrophe due to your initially "unharmful" data race.

As related questions, read How safe are Golang maps for concurrent Read/Write operations? and Incorrect synchronization in go lang.

Strongly recommended to read the blog post by Dmitry Vyukov: Benign data races: what could possibly go wrong?

Also a very interesting blog post which shows an example which breaks Go's memory safety with intentional data race: Golang data races to break memory safety

6
Yandry Pozo On

In terms of Race condition, it's not safe. In short my understanding of race condition is when there're more than one asynchronous routine (coroutines, threads, process, goroutines etc.) trying to access the same resource and at least one is a writing operation, so in your example we have 2 goroutines reading and writing variables of type function, I think what's matter from a concurrent point of view is those variables have a memory space somewhere and we're trying to read or write in that portion of memory.

Short answer: just run your example using the -race flag with go run -race or go build -race and you'll see a detected data race.

0
Quân Anh Mai On

The answer to your question, as of today, is that if a and b are not larger than a machine word, i must be equal to a or b. Otherwise, it may contains an unspecified value, that is most likely to be an interleave of different parts from a and b.

The Go memory model, as of the version on June 6, 2022, guarantees that if a program executes a race condition, a memory access of a location not larger than a machine word must be atomic.

Otherwise, a read r of a memory location x that is not larger than a machine word must observe some write w such that r does not happen before w and there is no write w' such that w happens before w' and w' happens before r. That is, each read must observe a value written by a preceding or concurrent write.

The happen-before relationship here is defined in the memory model in the previous section.

The result of a racy read from a larger memory location is unspecified, but it is definitely not undefined as in the realm of C++.

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.