Go test -race and uint8 variables


(Clay Douglass) #1

My code has a uint8 variable shared between two threads, one writes one read. Because uint8 access are inherently atomic I did not expect go to flag it as a race. Is there a real issue here is is this a deficiency in go -race detector?


(Jakob Borg) #2

There is a real issue. Among other things, cache coherency between CPUs, and compiler optimizations.

https://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong


(Dave Cheney) #3

The trick is there are two sorts of atomic update, the first, the one you mention states that so long as the value is naturally aligned (if it’s a 32 but value, say, then it is always written to an address which is a multiple of four) then another processor will not observe the value half written. The explain what that means, consider an unaligned write, where a 32 bit value is written to an address who’s bottom two bits are not zero. In this case the processor has to perform writes, spanning the boundary. Another processor can then observe this partially updated value. It’s colloqually known as a torn write. There is also a performance penalty for doing this, which is why many processors do not let you read and write unaligned data. This is their way of ensuring this first kind of atomic property.

However, as we’re talking about multiple processes we need to talk about visibility, caches, and write back. This is the second kind of atomic write. This states that for a value written to memory by one processor to be visible to another you need to use a memory fence. The name fence comes from the notion of stopping. The memory fence operation tells each processor that when the get to the fence they have to stop until thr memory writes they issues have been flushed to memory. The same is true for reads, you use a fence operation to tell the processor to stop and synchronise with any outstanding writes to memory. In this way a program can know that a value has been written to memory and visible to other processes. This is called publishing, we say a value is safely published if it uses the correct memory fence operations.

In terms of Go, memory fence operations are taken care of by the sync/atomic package, so you have to use that if you want to use a value in memory as a communication channel between two goroutines.


(Clay Douglass) #4

Thanks. That was helpful and clear.


(Philip Pearl) #5

Hi. I hope you don’t mind me jumping in here with another question I’ve been wondering about for a while.

I understand the atomic operations make access to the referenced variables atomic, but I’m unclear how that translates to other areas of memory.

For example, let’s imagine I’m crazy enough to use atomic.CompareAndSwapInt32 to hand-roll a mutex, and I use this to make sure that two threads don’t access a map at the same time. I can see how this really can make sure two threads don’t access that map at the same point. But what ensures that writes on one thread to the map are flushed through caches when the second thread is reading the map memory? atomic.CompareAndSwapInt32 isn’t passed a reference to the map memory, so for this to work, cache flushing must be global.

I think Mr. Cheney’s excellent answer above is implying that the fence operation the atomic functions implement does work globally (that is, it flushes all writes in all caches across all processors, not just any referenced by the atomic.CompareAndSwapInt32 parameters), but I’m sufficiently confused about this that I thought I would ask about this aspect explicitly.

Thanks in advance for any answers provided.

Phil


(Clay Douglass) #6

The atomic make use of special processor instructions like the lock prefix and the cas native instruction (x86 examples) that make sure the memory path (caches et.al) behave in the proper way. Google x86 memory barrier and x86 atomic ops and you’ll see all the gory details. If you open the implementation of the go atomic you can see the assembly language implementations.

The thing I was missing in my original post was appreciation for the need to insure the need to insure the memory path reflected my writes. the uin8 is atomic bu it’s natures but the changes may not be visible to other thread for an indefinite period, possible for every if you have a tight loop that keep it in a close cache that is not snooped by other cores.


(system) #7

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.