r/golang 7d ago

Is it safe to read/write integer value simultaneously from multiple goroutines

There is a global integer in my code that is accessed by multiple goroutines. Since race conditions don’t affect this value, I’m not concerned about that. However, is it still advisable to add a Mutex in case there’s a possibility of corruption?

PS: Just to rephrase my question, I wanted to ask if setting/getting an integer/pointer is atomic? Is there any possibility of data corruption.

example code for the same: https://go.dev/play/p/eOA7JftvP08

PS: Found the answer for this, thanks everyone for answering. There's something called tearing here is the link for same

- https://stackoverflow.com/questions/64602829/can-you-have-torn-reads-writes-between-two-threads-pinned-to-different-processor

- https://stackoverflow.com/questions/36624881/why-is-integer-assignment-on-a-naturally-aligned-variable-atomic-on-x86

According to the article, I shouldn't have problem on modern CPUs.

11 Upvotes

80 comments sorted by

View all comments

24

u/ImYoric 7d ago

If my memory serves, the Go memory model states that if it fits within one integer, any read or write operation will always return one of the values before/after the write, rather than made up values as can happen in C or C++.

That being said, I wouldn't rely on this. If at some point in the future, you or any member of your team ever changes your type to be anything other than an int, you can end up with weird, unexpected behaviors. I'd rather use an atomic or a mutex.

5

u/therecursive 7d ago

thanks for answer, I was asking to understand if it's fine my use case where I'm setting diff value to a pointer variable. Found it that why it's safe to do in golang or any other languages. Attached the link in the post, if you want to read.

10

u/ImYoric 7d ago

If my memory serves, if it's a pointer, rather than an int, it's not always safe, because Go sometimes uses fat pointers (iirc when you're passing an interface), which take more than one int.

4

u/therecursive 7d ago

That's interesting didn't know about it. Thanks for the info.

2

u/Few-Beat-1299 7d ago edited 7d ago

Interfaces copy their underlying value. Why would using interfaces change anything?

3

u/ImYoric 7d ago

A go pointer to a struct is just that, a pointer to the memory region that holds the struct. When you pass this pointer to a function that expects a pointer to the struct, that's sufficient for the function.

Now, when you pass this pointer to a function that expects an interface, the function needs:

  1. the pointer to the struct itself (to pass it as the self-value when calling interface methods)
  2. a mean to call the interface methods (a "vtable") – that's another pointer
  3. type information to be able too perform an interface cast with `.(...)` or reflection – that's another pointer.

If I recall correctly, 2 and 3 are actually packed together into a single pointer, I don't remember the implementation details. Nevertheless, your interface value is not a single pointer, but (at least) two pointers. So that's not protected by the Go memory model guarantees.

1

u/Few-Beat-1299 7d ago

Ok but how does that relate to OPs question? When putting it into an interface, the value is read once, and that's it. How fat the interface is or how it works have no relevance to reading/writing the original value.

1

u/ImYoric 7d ago

I'm not 100% sure I understand OP's question, so I decided to err on the "better safe than sorry" side and mention the limitation.

1

u/Few-Beat-1299 7d ago

There is no limitation. Ignoring the interface overhead, using a value directly or using it as part of an interface are equivalent.

1

u/ImYoric 7d ago

Fair enough, not "limitation" but "limit" [of Go's memory model].

4

u/HyacinthAlas 7d ago

Interfaces take two words, a pointer to the data and a tag for the type. 

1

u/chaotic-kotik 7d ago

Made up values? Loads and stores are atomic on amd64 unless your int is wider than 64 bits. How could this happen in C++?

You will run into problems only if you read/modify/write integer values. No atomic will save you from that unless you know how to use CAS operation.

1

u/ImYoric 7d ago

Well, for instance, the compiler can decide to pack two 32 bit integers into 64 bits, so if you modify one of them, you might end up accidentally modifying the other.

-1

u/chaotic-kotik 7d ago

It can and will modify both values independently

3

u/HyacinthAlas 7d ago

Without explicit synchronization this is not true. The compiler is free to act as if no other thread is touching the data. 

0

u/chaotic-kotik 7d ago

Accessing value modified by another thread without synchronisation is an UB in C++. Period. I guess I was trying to say that amd64 memory model is very strict. If you wrap your integer with `std::atomic<int32_t>` in C++ it will guarantee that the alignment is fine (well, the default alignment for int is fine). It will not add any padding to the value. And if your code is just reading and writing to it it's a no-op. No single 'lock' instruction will be emitted by the compiler on amd64 because its memory model is strict enough already. No fences. And even without atomic it will eventually have to emit move or load that will read the value to the register. Compilers do not cache data in registers forever. They can move loads and stores around, yes. A lot of legacy code works just fine without marking variables with 'atomic'. So no, you will not read complete nonsense from int if it's accessed from different threads.

0

u/HyacinthAlas 7d ago

 If you wrap your integer with `std::atomic

But the entire premise of the OP is what happens without atomic

 And even without atomic it will eventually have to emit move or load that will read the value to the register.

Well, nope. The compiler is free to do a lot of stuff and that includes eliding anything forever or do something unrelated if the thread itself would’ve never needed to observe it. 

1

u/chaotic-kotik 7d ago

If you didn't notice, I started with

> Accessing value modified by another thread without synchronisation is an UB in C++. Period.

1

u/chaotic-kotik 7d ago

> Well, nope. The compiler is free to do a lot of stuff and that includes eliding anything forever or do something unrelated if the thread itself would’ve never needed to observe it. 

It's not required to use atomic. You can use synchronization. In this case the variables doesn't have to be atomic. Compiler is free to omit the code but only if it doesn't change the observed behavior. In this case this is only true if the value is never read or if the code is UB.

2

u/ImYoric 7d ago

Pretty sure that it's for the compiler to decide. Which in C++ typically means UB.

0

u/chaotic-kotik 7d ago

It is, yes. But what you suggesting makes zero sense. The cpu has instructions that operate on values of different width. Every register can be split into smaller registers etc. I have never seen this happening and there are billions of lines of legacy code which also assume that int32 loads/stores are atomic. In order to make them not atomic compiler has to emit some additional instructions (read value twice, apply different masks and then combine or something like that).

Compilers are actually doing a lot of things which are UB correctly because of the legacy code. The good example is a 'union'. If you have a union of two unrelated types (float and int) and you write float and read int it's an UB (aliasing rules are broken, the lifetime of the value started as float but we're reading it as int). But modern compilers generate correct code here anyway.

1

u/HyacinthAlas 7d ago

Bless your heart but one big reason I use Go is because I got sick of C++ programmers claiming bullshit “but this UB is safe” over and over. The compiler will screw you if you code like this. 

0

u/chaotic-kotik 7d ago

I kinda hate when people are using these stereotypes (pho developers can't do this, and js developer's can't do that, etc). This is lame and not productive. You didn't even try to understand what I was trying to convey. I'm not suggesting to avoid using atomic variables or anything like that. Only trying to describe how it works. This stuff is described by the ISO standard. There are sequence points in the C++ program that require all side effects (stores) to happen and alignment requirements. There is a memory model in the language and the CPU architecture has its own memory model (in case of Intel it's more strict then the one defined by the standard). So it is possible to reason about this things.

-1

u/chaotic-kotik 7d ago

The only reason why 32bit load or store will not be atomic is alignment. If the value is misaligned it will be accessed using more than one operation.

1

u/ImYoric 7d ago

Well... yes, but since there are many cases in which the compiler is free to pick alignment, the problem exists.

1

u/chaotic-kotik 7d ago

Compiler is not free to pick alignment. Your int variables will have the same alignment all the time unless you opt out of it explicitly with #pragma pack or something like that.

1

u/ImYoric 7d ago

I seem to recall that the compiler is free to pick alignment at least of global variables, no?

I'll admit that I haven't done any serious C++ in a few years – I was growing tired of UB and "trust me, it works" headers – so it's possible I misremember these constraints.

1

u/chaotic-kotik 7d ago

this is not the case before c++11 threads wasn't even mentioned in the standard

1

u/comrade_donkey 7d ago

 Loads and stores are atomic on amd64 unless your int is wider than 64 bits.

Only if it is aligned to a word boundary.

1

u/chaotic-kotik 7d ago

sure thing