In what cases do I need to block a variable from simultaneous access?

In a C or C ++ program, if 2 threads use the same global variable, you need to block var through the mutex.

But in what cases exactly?

  • Topic 1: Read Topic 2: Read
  • Topic 1: write Topic 2: read
  • Topic 1: write topic 2: write

Of course, you need to block in case 3, but what about the other 2 cases? What happens in case 2 (with non-atomic operations)? Is there any kind of access violation or is Thread 2 just getting the old value? Am I a little confused because the memory and registers on the hardware level cannot be accessed simultaneously (in the usual PC hardware) or do we have any parallel processors with parallel bus lines for parallel RAM chips?

+6
source share
4 answers

Think about what might happen in each case. Let's consider only the condition of the race: it is easy, and we just need to see the consequences.

In case 1, the variable does not change, so no matter what order, both streams will read the same value. So basically, there is nothing wrong with that.

Cases 2 and 3 are worse. Let's say you have a race condition, and I don’t know which thread will get access earlier. It means:

In case 2: the value of the variable at the end of all operations is fine (it will be the value recorded in Thread 1), but Thread 2 can get the old value of the variable, which can lead to failure, or other problems.

In case 3: The final value of the variable is not predictable, since it depends on which stream will perform the write operation last.

In case 2 and 3, it can also happen that one of the threads tries to access the variable while it is in an inconsistent state, and you can get some garbage data read by one of the threads (for example, Case 2) or even garbage data in variable after all operations are completed.

So yes, blocking for cases 2 and 3.

+8
source

The blocking rules are simple:

If you are writing a variable that is being accessed by another thread at the same time, then you need the correct ordering of operations (for example, through locking).

With this simple rule, we can easily evaluate each of the cases:

  • No recording, no synchronization needed.
  • Recording, concurrent access, synchronization required
  • Recording, concurrent access, synchronization required

And what happens if you don’t block?

Well, formally this behavior is undefined. This means that we do not know. Which, although the only possible answer helps to understand the extent of the problem.

At the machine level, either the following can happen:

  • read: access to deprecated value
  • when reading: access to partial value (only half is updated while you are watching)
  • in case of writing: the final value is a hash (at the bit level) of the values

... and don’t forget that whenever you read the wrong pointer / size, it may crash.

At the compiler level, the compiler can optimize as if the thread had only access. In this case, this may mean:

  • removing redundant readings: converting while (flag) to if (flag) while (true) ...
  • delete "unused" entries
  • ...

The presence of memory barriers and explicit synchronization instructions (which mutexes use) prevent these optimizations.

+3
source

The rule is simple: if an object is accessed (read or written) by more than one stream, and it is modified by any stream, then all access must be synchronized. Otherwise, you have undefined behavior.

+2
source

It is not as simple as it may seem, except in two cases:

  • When there are only readers and no writers, you need to not sync.
  • When writing multiple threads, and at least one of them performs a read-modify-write operation (for example, ++x; ), you need to always synchronize, or you will get completely unpredictable results.

In all other cases, if you have at least one writer and reader (or several authors), you usually (with very few exceptions) should synchronize access, but not always, and not always in the most strict way.

It depends on what kind of warranty you need. Some applications require a strict consistent consistency across threads (and sometimes even block integrity is needed). Some applications will work equally well, but with much greater performance, if they only happen - before guarantees in a single thread. However, other applications do not even need it and are completely satisfied with relaxed operations or without any guarantees whatsoever.

For example, this “typical” workflow implementation has an author and a reader:

 // worker thread running = true; while(running) { task = pull_task(); execute(task); } // main thread exit code running = false; join(workerthread); 

This works fine, without any synchronization. Yes, pedantically speaking, it is undefined when and how the running value will change, but in fact it does not matter. It is impossible for the memory location to have any “random” intermediate value, and it really doesn’t matter if the change becomes visible several tens of nanoseconds sooner or later, because the workflow is likely to be busy with the task in any in the worst case, it picks up the change a few milliseconds later. In the end, at the next iteration, the workflow will pick up the change and exit.
SPSC fast forward, published in Dr. Dobb several years ago, worked on a similar principle, only with pointers.

A good and comprehensive introduction to the various synchronization modes and their consequences is given in the GCC documentation .

0
source

Source: https://habr.com/ru/post/950906/


All Articles