C++ Atomic Variables
In the previous article, you leaned how to coordinate critical work. Managing work often requires flags that determine the state of the task. In this article you’ll learn how to safely use flags during multi-threaded operations.
Imagine a number of concurrent threads are mining Litecoin on a GPU. As soon as more than one thread writes to the same data at the same time, such as creating a new block at the same time, a race condition can occur. Race conditions cause data corruption vulnerabilities. Another example is the window in time between when you apply a security control and when you use it. During a race condition, an attacker may be able to tamper with a security flag or change a shared resource that ultimately changes the flow of security code happening on another thread.
An atomic variable is one where the load or store execute with a single instruction. It prevents an attacker intentionally slipping in steps between the save and load of a security flag.
Often you’ll create an atomic variable by using wrapping functions and locking a mutex. The OS may not perform that within a single instruction and that can be slow. Since C++ 11, there is an optimized std::atomic type.
Using Atomic
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 platform implements atomic objects using faster, lock-free operations. If you’re storing an object containing a few megabytes, you’ll want to stick with a mutex. 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, the system works with a “complete object” when it writes an atomic object on one thread and reads it from another thread at the same time. In contrast, a non-atomic operation may return a portion of the object while it updates it on another thread at the same time.
An atomic object has a deleted copy constructor. You can not copy or move it. 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 function updates the value that you passed with the actual value of the stored atomic variable. These functions return true if performed an exchange.
The default constructor of the atomic type will leave the object uninitialized, so you may want to use the object’s initialization constructor. Here’s a global atomic variable. You can construct instances from non-atomic variables:
std::atomic<bool> gUserLoggedIn(false);
Declaring Atomic Inside a Class
Defining an atomic variable inside a class takes more care:
#import <atomic>
class BankMachine
{
private:
std::atomic<bool> _isProcessingTransaction;
};
To access the variable atomically, use the load
and store
member functions. You might create setters and getters to do that:
void BankMachine::setProcessing(bool isProcessing)
{
_isProcessingTransaction.store(isProcessing);
}
bool BankMachine::isProcessing()
{
return _isProcessingTransaction.load();
}
Using accessor methods is a good way to keep security-specific operations in one place. This is especially true if setting a variable requires a few steps you might forget each time you need to set the variable.
In most cases, this is all you need to do to set up the std::atomic
variable. Because you included it as a member variable inside a class, remember that the copy assignment operator is deleted for std::atomic
. That now means you can’t assign your class because the copy assignment operator is implicitly deleted. Right now this will not compile:
BankMachine bankMachine;
_bankMachine = bankMachine;
Go ahead and fix the issue. While you’re at it, 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;
};
};
Using Atomic Operators
You can also use operators for a specific type on std::atomic
. For example, with an int
you can do this:
std::atomic<int> numberOfTransactions(0);
++numberOfTransactions;
--numberOfTransactions;
Using the std::atomic
template class will help prevent your concurrent code from being susceptible to data race vulnerabilities. Using atomic variables does not mean your code becomes thread-safe. Careful concurrent programming design is still needed when writing concurrent code.
Check out the reference for more information about this class. If you’re working with concurrency on mobile, check out the Secure Coding with Concurrency in Swift tutorial.