main.cpp
Code: Select all
static int x = 0;
Code: Select all
// If thread 2 changed x first.
if ( x == 15 )
{
std::cout << "Thread 2 executed first!\n";
}
x = 24;
Code: Select all
// If thread 1 changed x first.
if ( x == 24 )
{
std::cout << "Thread 1 executed first!\n";
}
x = 15;
The changes you made in one thread are always going to be observed to happen in the same order you made them in the same thread. The problem comes when another thread then tries to observe those changes. Not only is it not guaranteed to see the changes happen in the same order as the thread that made them, the changes it then makes may be observed to happen in a different order in the original thread too. Fortunately for us, C++ already has a construct that helps us maintain the atomicity of our changes across threads. Meet std::atomic! Atomic is a variable that gives you the guarantee that the changes made by any thread to that atomic variable is part of a single atomic operation that is shared among all threads. In other words, the changes any thread makes to it will be observed to happen in the same order for all threads. Not only that, it can also be customised to give you several additional levels of guarantees. std::memory_order_relaxed is the most basic guarantee the atomic provides. This guarantee is more than enough to fix our previous example:
main.cpp
Code: Select all
static std::atomic<int> x { 0 };
Code: Select all
// If thread 2 changed x first.
if ( x.load ( std::memory_order_relaxed ) == 15 )
{
std::cout << "Thread 2 executed first!\n";
}
x.store ( 24, std::memory_order_relaxed );
Code: Select all
// If thread 1 changed x first.
if ( x.load ( std::memory_order_relaxed ) == 24 )
{
std::cout << "Thread 1 executed first!\n";
}
x.store ( 15, std::memory_order_relaxed );
main.cpp
Code: Select all
static int value = 0;
static std::atomic<bool> written { false };
Code: Select all
value = 15;
// Tell the reader thread that we have written the value.
written.store ( true, std::memory_order_relaxed );
Code: Select all
// Wait until the value is written by the writer thread.
while ( written.load ( std::memory_order_relaxed ) == false )
{
continue;
}
std::cout << value << '\n';
Writer Thread
Code: Select all
value = 15;
// Release our changes to the reader thread.
written.store ( true, std::memory_order_release );
Code: Select all
// Acquire the changes made by the writer thread.
while ( written.load ( std::memory_order_acquire ) == false )
{
continue;
}
std::cout << value << '\n';
main.cpp
Code: Select all
std::vector<Object> objects;
static std::atomic<std::size_t> num_threads_left { 0 };
Code: Select all
void update_objects ( const std::size_t begin, const std::size_t end )
{
// Update the part of the object vector assigned to this thread.
for ( auto it = begin; it != end; ++it )
{
it->update ( );
}
// Acquire changes other threads have made. Then, release our changes.
const auto old_value = num_threads_left.fetch_sub ( 1, std::memory_order_acq_rel );
const auto current_num_threads_left = old_value - 1;
// If we are the last thread to finish
if ( current_num_threads_left == 0 )
{
std::cout << "All threads have finished! Returning to main thread!\n";
}
// Otherwise, wait until all threads have finished.
else
{
while ( num_threads_left.load ( std::memory_order_acquire ) != 0 )
{
continue;
}
}
}
main.cpp
Code: Select all
#include <thread>
#include <atomic>
#include <cassert>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst))
;
if (y.load(std::memory_order_seq_cst)) {
++z;
}
}
void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst))
;
if (x.load(std::memory_order_seq_cst)) {
++z;
}
}
int main()
{
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join(); b.join(); c.join(); d.join();
assert(z.load() != 0); // will never happen
}
If you made it this far, congrats! You are one of the top 5% of programmers that actually understand the niche that is Lock Free Programming. Most people would just use std::mutex to "block" threads forcing only one thread be able to read and write shared values at a time. There's no denying the power of std::atomic provides by giving you the ability to make changes without having to block any thread. If used correctly, it can vastly improve the performance of your multithreaded code since every thread can continue to do work without blocking each other from progressing. Used incorrectly however, it can be much slower than simply blocking and more importantly, may cause huge untraceable bugs. Like all tools in C++, it must be used wisely.