Should integer reading be a critical sector?

I came across C ++ 03 which takes this form:

struct Foo { int a; int b; CRITICAL_SECTION cs; } // DoFoo::Foo foo_; void DoFoo::Foolish() { if( foo_.a == 4 ) { PerformSomeTask(); EnterCriticalSection(&foo_.cs); foo_.b = 7; LeaveCriticalSection(&foo_.cs); } } 

Is it necessary to protect reading from foo_.a ? eg:.

 void DoFoo::Foolish() { EnterCriticalSection(&foo_.cs); int a = foo_.a; LeaveCriticalSection(&foo_.cs); if( a == 4 ) { PerformSomeTask(); EnterCriticalSection(&foo_.cs); foo_.b = 7; LeaveCriticalSection(&foo_.cs); } } 

If so, why?

Suppose integers are 32-bit. The platform is ARM.

+8
c ++ concurrency winapi critical-section
source share
5 answers

Technically yes, but no on many platforms. First, suppose int is 32 bits (which is pretty common, but not nearly universal).

It is possible that two words (16 bits) of 32 bits of int will be read or written separately. On some systems, they will be read separately if the int misaligned.

Imagine a system in which you can only execute 32-bit aligned 32-bit reads and writes (and 16-bit aligned 16-bit reads and writes) and int that cross such a boundary. The int is initially zero (i.e. 0x00000000 )

One thread writes 0xBAADF00D to int , the other reads it "at the same time".

The writing stream first writes 0xBAAD to the upper word int . Then the read stream reads all int (both high and low), getting 0xBAAD0000 - this is the state in which int never put into action!

Then, the recording stream writes the bottom word 0xF00D .

As already noted, on some platforms, all 32-bit reads / writes are atomic, so this is not a problem. However, there are other problems.

In most cases, the lock / unlock code contains instructions for the compiler to prevent lock reordering. Without this prevention of reordering, the compiler can freely reorder things, as long as it behaves "as is" in a single stream context, it would work that way. Therefore, if you read a and then b in the code, the compiler could read b before it reads a if it does not see that the in-thread capability for b should be changed at that interval.

Thus, it is possible that the code you are reading uses these locks to ensure that the variable is being read in the order specified in the code.

Other questions are covered in the comments below, but I don’t feel competent in solving them: cache problems and visibility.

+9
source share

Looking at this , it seems that the hand has a rather relaxed memory model, so you need a form of memory barrier to ensure that entries in one thread are visible when you expect them in another thread. Therefore, what you are doing, or using std :: atomic, is probably necessary on your platform. If you do not take this into account, you will see that the updates are out of order in different threads that would violate your example.

+3
source share

I think you can use C ++ 11 to ensure that integer reads are atomic using (for example) std::atomic<int> .

+2
source share

The C ++ standard says that there is a data race if one stream writes to a variable while reading another stream from this variable, or if two streams are written to the same variable at the same time. The following states that data race causes undefined behavior. So, formally, you should synchronize these reads and records.

There are three separate problems when one stream reads data that was written by another stream. Firstly, tearing occurs: if more than one bus cycle is required for recording, it is possible that the stream switch is in the middle of the operation, and the other stream may see a semi-recorded value; there is a similar problem if reading requires more than one bus cycle. Secondly, visibility: each processor has its own local copy of the data that it recently worked on, and writing to one processor cache does not necessarily update the other processor cache. Thirdly, there are compiler optimizers that reorder reading and writing in ways that will be in order in a single thread, but will break multi-threaded code. Version-protected code must deal with all three issues . This is the task of synchronization primitives: mutexes, condition variables, and atomistics.

+2
source share

Although the whole read / write operation is likely to be atomic, compiler optimization and processor cache will still cause problems if you do not do it right.

To explain, the compiler usually assumes that the code is single-threaded and does many optimizations that rely on it. For example, this may change the order of instructions. Or, if he sees that the variable is written and never read, it can fully optimize it.

The processor will also cache this integer, so if one thread writes it, the other may not see it until the end later.

There are two things you can do. One of them is to enter a critical section, as in your source code. Another is to mark the variable as volatile . This will signal to the compiler that several threads will be available to this variable, and will disable a number of optimizations, and also place special synchronization caching commands (also called "memory barriers") around the variable access (or, as I understand it). This seems to be wrong.

Added: In addition, as another answer noted, Windows has Interlocked APIs that can be used to prevent these problems for volatile variables.

0
source share

All Articles