Can someone please explain what the <atomic> library does? Or what it's for?

Recommended Answers

All 6 Replies

It allows you to do atomic operations on variables.

I'm sorry, I'm having trouble understanding what it is necessarily used for? Is it sort of like a pre-processor directive, but within the actual code?

I'm having trouble understanding what it is necessarily used for?

Consider this piece of C++11 code:

#include <future>
#include <atomic>
#include <vector>
#include <iostream>

int main() {
  int value = 0;

  std::vector< std::future< int > > results;
  results.emplace_back( std::async( [&value]() -> int { return value += 2; } ) );
  results.emplace_back( std::async( [&value]() -> int { return value += 3; } ) );

  for(auto& x : results)
    std::cout << x.get() << " ";
  std::cout << value << std::endl;
};

If you are not familiar with C++11 concurrency features (like std::future and std::async) I recommend you look it up. In essence, this code creates two threads (via async) that will execute a function that increments a shared value (value) by either 2 or 3, respectively. Then, the results are printed out, and then, the final value of the shared value is printed too.

The million dollar question here is: What will this code print out?

The stupid answer:
It should print out 2 5 5. If you have no idea what threads are or how they work, you might think that this code is obvious: start with (value == 0), then increment it by 2 and store the result in the vector, then increment it by 3 and store the result in the vector, and finally, the value ends up with a value of 5. However, you can't rely on the "first" thread executing "first", because they can either execute simultaneously or in any order.

The naive answer:
It will either print out 2 5 5 or 5 3 5, depending on which thread executed first. At least, this considers the fact that the threads might execute in any order, meaning that the shared value might first be incremented by 3 (returning 3 from the second future) before being incremented by 2 (returning 5 from the first future). And on most systems and most times you run it, this answer will actually be correct, however, it is not strictly 100% guaranteed to be either of these results.

The educated answer:
It will either print out 2 5 5, 5 3 5, 2 3 3 or 2 3 2. The first two are the same cases as above. The other two cases are the result of the += operation on the integer not being guaranteed to be atomic. If the increments are not atomic operations, then there is a possibility that both threads simultaneously read an original value of 0, and then, they each increment the value and return and store the results, leaving the shared variable with the return value of whichever thread finished last.

The correct answer:
It is undefined behavior. There is no guarantee as to what this code will output. This is again a consequence of the operations not being atomic. There is a possibility (however remote) that even the reading or writing of the values are not atomic, meaning that the two threads could interfere directly with each other's read/write operations. This could lead to frankenstein values containing half the result of one thread and half of the other, and all other similar sorts of mayhem.

By making the shared variable an atomic variable, you can bring things back to sanity. If you have the variable as this:

std::atomic< int > value{0};

then the variable value is essentially just an integer value, except that it has a number of operations (functions and operator overloads) that come with atomicity guarantees.

For one, if you were to perform the increments like this:

  results.emplace_back( std::async( [&value]() -> int { 
    int r = value.load() + 2;
    value.store( r );
    return r;
  } ) );
  results.emplace_back( std::async( [&value]() -> int { 
    int r = value.load() + 3;
    value.store( r );
    return r;
  } ) );

then you get rid of the undefined behavior. Now, you basically get the situation described as the "educated answer". You are guaranteed that the reading and writing of the values will be done correctly, you just can't be sure of the order and if there will be an interleaving between the two threads.

Then, if you simply use the std::atomic operator for +=, which has a complete atomicity guarantee (the read, increment, and write will all happen in one uninterruptable operation). As so:

  results.emplace_back( std::async( [&value]() -> int { 
    return value += 2;
  } ) );
  results.emplace_back( std::async( [&value]() -> int { 
    return value += 3;
  } ) );

with this, you fall under the description in the "naive answer". The only real guarantee you have is that the final value of the shared variable will be 5, but that is typical of doing a number of asynchronous operations.

Now, imagine above program was for a banking system, and each "thread" is actually a transaction being done on the same account. In that example, it is pretty clear that atomicity of the transactions is necessary for not losing any money (guarantee the end result), while letting operations occur in unspecified order.

Is it sort of like a pre-processor directive, but within the actual code?

No. This is mostly just ordinary C++ code (not pre-processor thing or a built-in thing). This is what is typically referred to as a "value wrapper" or just a "wrapper". Some may also call this a "decorator". It is just that, a class template (std::atomic<T>) that wraps a value with some guaranteed "atomic" operations, and a set of functions for specific and typical atomic operations. Atomic operations are mainly used for kernel-level code and other such low-level high-performance and high-reliability coding (what we call "system" or "infrastructure" programming), the average programmer doesn't even know they exist.

commented: Great post! +8

Michael, that was an awesome post!

@mike_2000_17 Sorry I didn't reply sooner. Thank you for that amazing post. I understand now and I see how to declare it and its syntax among other things. Again, thank you so much for replying with that post.

Mike2K rocks! Thanks for the post Mike. I haven't pulled myself up to speed on the new standard. This helps, and I WILL do some dilligence on it... :-)

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.