As Timur says in his talk then strictly speaking this has a number of pitfalls. (Which is probably why — TL;DR — it will have to wait until C++20.)
But if you’re sure that your floats will not throw exceptions, maybe keep them within ±INF and not be NaNs then you could roll your own.
Something like this:
static std::atomic<float>& operator+= (std::atomic<float>& atomicFloat, float increment)
{
float oldValue;
float newValue;
do
{
oldValue = atomicFloat.load (std::memory_order_relaxed);
newValue = oldValue + increment;
} while (! atomicFloat.compare_exchange_weak (oldValue, newValue,
std::memory_order_release,
std::memory_order_relaxed));
return atomicFloat;
}
Then some test code that fires up a bunch of threads which each increment the atomic value a number of times:
int main (int argc, char* argv[])
{
constexpr int numThreads = 16;
constexpr int numIters = 1000;
constexpr float increment = 0.1f;
float nonAtomicValue (0.0f);
for (int i = 0; i < numThreads; ++i)
for (int j = 0; j < numIters; ++j)
nonAtomicValue += increment;
std::cout << "single thread nonAtomicValue = " << nonAtomicValue << "\n";
nonAtomicValue = 0.0f; // reset
std::atomic<float> atomicValue (0.0f);
std::vector<std::thread> threads;
for (int i = 0; i < numThreads; ++i)
{
threads.emplace_back ([&]()
{
for (int j = 0; j < numIters; ++j)
{
atomicValue += increment; // safe for most values atomicValue might hold
nonAtomicValue += increment; // unsafe
// deliberately throw the threads out of sync
std::this_thread::sleep_for (std::chrono::nanoseconds (i * j + 1));
}
});
}
for (auto& thread : threads)
thread.join();
std::cout << "multithread nonAtomicValue = " << nonAtomicValue << "\n";
std::cout << "multithread atomicValue = " << atomicValue.load (std::memory_order_relaxed) << "\n";
return 0;
}
Example output:
single thread nonAtomicValue = 1599.76
multithread nonAtomicValue = 1599.16
multithread atomicValue = 1599.76
The “multithread nonAtomicValue” value will almost always be incorrect.
The issue Timur mentions about the problems with float precision and the compare_exchange_xxx()
functions should be avoided here as the bit pattern of the oldValue
float should be identical to the contents of the atomic when the comparisons performed by compare_exchange_weak()
. Of course we’re in trouble if oldValue
is a NaN, since comparing NaNs always returns false! I deliberately picked a value of 0.1 in the code above as that is not perfectly representable as a float.
A simple example of the kind of trouble you can get in with comparing equality using floats is:
int main (int argc, char* argv[])
{
float a = 0.0f, b = 0.1f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
a += 0.01f;
std::cout << "a = " << a << "\n";
std::cout << "b = " << b << "\n";
std::cout << "a==b = " << (a == b ? 1 : 0) << "\n";
return 0;
}
Where a and b are both printed as being “0.1” but also shown to be unequal!