Atomic

01 Mar 2013

#C++ Atomic Programming

Often we try and create an atomic function by locking a mutex which can be slow. Now with C++ 11, there is an optimized std::atomic type. std::atomic is a template class, so that you can use whichever type you want to create an atomic variable. The benefit of this class is performance. For speed and in most cases, especially with primitive data types, the atomic objects are implemented using faster, lock-free operations. If you are storing an object containing a few megabytes, then a mutex may still be used. Generally speaking, the specific platform implementation of this API decides when and when not to use lock-free techniques. You can see if a particular type uses a lock by calling the is_lock_free() member function.

The objective of std::atomic types are to create objects that are free from data races; particularly to make operations on an object indivisible. For example, a “full object” is computed when an atomic object is written to on one thread, and read from another thread at the same time. In contrast, a non-atomic operation may have a portion of the object returned while it is being updated by another thread at the same time.

An atomic object has a deleted copy constructor. It can not be copied or moved. Since these objects are not copyable or assignable, they have an exchange() function, as well as compare_exchange_weak() and compare_exchange_strong() functions which compare the value stored with the one passed in to the function and only replace the stored value if they are equal. If they are not equal, the value that was passed into the function is updated with the actual value of the stored atomic variable. These functions return true if an exchange was performed.

The default constructor of the atomic type will leave the object uninitialized, so you may want to use the object’s initialization constructor, for example

//Here is a global atomic variable. Instances can be constructed from a non-atomic variable.
std::atomic<bool> gUserLoggedIn(false);

Defining an atomic variable inside a class will take a bit more care. Lets do that now:

#import <atomic>
class BankMachine
{
private:
    std::atomic<bool> _isProcessingTransaction;
};

In order to access the variable atomically, we use the load() and store() member functions. For example, you might create setters and getters to do that as such:

void BankMachine::setProcessing(bool isProcessing)
{
    _isProcessingTransaction.store(isProcessing);
}

bool BankMachine::isProcessing()
{
    return _isProcessingTransaction.load();
}

Using accessor methods is a good way to keep these more specific operations in one place, especially if setting a variable requires a few steps which one might forget to do each time they have to set the variable.

In most cases, this would be all you need to do to set up the std::atomic variable. However, because we included it as a member variable inside a class, remember that the copy assignment operator is deleted for std::atomic. The entire class can not just be assigned because the copy assignment operator is implicitly deleted. For example, right now this will not compile:

BankMachine bankMachine;
_bankMachine = bankMachine;

Lets go ahed and fix the issue, and while we are at it, we might as well write constructors and a destructor given the C++ Rule Of Three

class BankMachine
{
private:
    std::atomic<bool> _isProcessingTransaction;
    
public:
    
    BankMachine()
    {
        _isProcessingTransaction.store(false);
    }
    
    BankMachine(const BankMachine &source)
    {
        _isProcessingTransaction.store(source._isProcessingTransaction.load());
    };
    
    ~BankMachine()
    {
        _isProcessingTransaction.store(false);
    };
    
    BankMachine& operator =(BankMachine &source)
    {
        _isProcessingTransaction.store(source._isProcessingTransaction.load());
        return *this;
    };
};

The atomic class can also utilize operators for it’s specific type, for example, with an int we can do this:

std::atomic<int> numberOfTransactions(0);

++numberOfTransactions;
--numberOfTransactions;

And that’s it for a quick walk through of the std::atomic template class. Note that just including atomic variables does not mean your variable becomes thread-safe. Careful concurrent programming design is still needed when using threads. For more information about this class, check here