• C++ Programming for Financial Engineering
    Highly recommended by thousands of MFE students. Covers essential C++ topics with applications to financial engineering. Learn more Join!
    Python for Finance with Intro to Data Science
    Gain practical understanding of Python to read, understand, and write professional Python code for your first day on the job. Learn more Join!
    An Intuition-Based Options Primer for FE
    Ideal for entry level positions interviews and graduate studies, specializing in options trading arbitrage and options valuation models. Learn more Join!

Free course: C++ 11 to 20, std::atomic memory orders.

ptf

Joined
3/29/21
Messages
20
Points
13
In this video, let's compare the different kinds of atomic's memory orders: relaxed, consume, acquire, release, sequentially consistent, and understand their differences and when to use them.


C++:
// Video lecture: https://www.youtube.com/watch?v=UzYVDki31Hg

#include <iostream>
#include <atomic>
#include <thread>
#include <random>
#include <vector>
#include <cassert>
// #include "jthread.hpp"

using namespace std::chrono_literals;

void test_atomic_relaxed() {
    // Atomic operations tagged memory_order_relaxed are not synchronization operations; they do not
    // impose an order among concurrent memory accesses. They only guarantee atomicity and
    // modification order consistency.
    std::atomic<int> x = {0};
    std::atomic<int> y = {0};
    std::jthread t1([&]() {
        auto r1 = y.load(std::memory_order_relaxed);  // A
        x.fetch_add(r1, std::memory_order_relaxed);   // B
    });

    std::jthread t2([&]() {
        auto r2 = x.load(std::memory_order_relaxed);  // C
        y.fetch_add(42, std::memory_order_relaxed);   // D
    });
    // Possible outcomes
    // CDAB: r1 = 42, r2 = 0
    // ABCD: r1 = 0, r2 = 42
    // DABC: r1 = 42, r2 = 42
    // ...
}

void test_consume_release() {
    // If an atomic store in thread A is tagged memory_order_release and an atomic load in thread B
    // from the same variable that read the stored value is tagged memory_order_consume, all memory
    // writes (non-atomic and relaxed atomic) that happened-before the atomic store from the point
    // of view of thread A, become visible side-effects within those operations in thread B into
    // which the load operation carries dependency, that is, once the atomic load is completed,
    // those operators and functions in thread B that use the value obtained from the load are
    // guaranteed to see what thread A wrote to memory.

    // The synchronization is established only between the threads releasing and consuming the same
    // atomic variable. Other threads can see different order of memory accesses than either or both
    // of the synchronized threads.

    std::atomic<std::string*> ptr{};
    int data;
    std::string* p{};
    std::jthread producer([&]() {
        p = new std::string("Hello");
        data = 42;
        ptr.store(p, std::memory_order_release);
    });

    std::jthread consumer([&]() {
        std::string* p2{};
        while (!(p2 = ptr.load(std::memory_order_consume)))
            ;
        assert(*p == "Hello");   // always true
        assert(*p2 == "Hello");  // always true: *p2 carries dependency from ptr
        assert(data == 42);      // may and may not be true: data does not carry dependency from ptr
        delete p2;
    });
}

void test_acquire_release_1() {
    // If an atomic store in thread A is tagged memory_order_release and an atomic load in thread B
    // from the same variable is tagged memory_order_acquire, all memory writes (non-atomic and
    // relaxed atomic) that happened-before the atomic store from the point of view of thread A,
    // become visible side-effects in thread B. That is, once the atomic load is completed, thread B
    // is guaranteed to see everything thread A wrote to memory.

    std::atomic<std::string*> ptr{};
    int data;
    std::string* p{};
    std::jthread producer([&]() {
        p = new std::string("Hello");
        data = 42;
        ptr.store(p, std::memory_order_release);
    });

    std::jthread consumer([&]() {
        std::string* p2{};
        while (!(p2 = ptr.load(std::memory_order_acquire)))
            ;
        assert(*p == "Hello");   // always true
        assert(*p2 == "Hello");  // always true
        assert(data == 42);      // always true
        delete p2;
    });
}

void test_acquire_release_2() {
    // If an atomic store in thread A is tagged memory_order_release and an atomic load in thread B
    // from the same variable is tagged memory_order_acquire, all memory writes (non-atomic and
    // relaxed atomic) that happened-before the atomic store from the point of view of thread A,
    // become visible side-effects in thread B. That is, once the atomic load is completed, thread B
    // is guaranteed to see everything thread A wrote to memory.

    std::vector<int> data;
    std::atomic<int> flag = {0};
    std::jthread producer([&]() {
        data.push_back(42);
        flag.store(1, std::memory_order_release);
    });

    std::jthread consumer([&]() {
        int expected = 1;
        // Compares the contents of the flag with expected:
        // - if true, it replaces the flag value with 2. (performs read-modify-write operation)
        // - if false, it replaces expected with the flag. (performs load operation)
        // returns
        // - true if expected compares equal to the contained value.
        // - false otherwise.
        while (!flag.compare_exchange_weak(expected, 2, std::memory_order_acq_rel)) {
            expected = 1;
        }
        assert(data.at(0) == 42);  // always true
    });
}

void test_seq_cst() {
    // A load operation with this memory order performs an acquire operation, a store performs a
    // release operation, and read-modify-write performs both an acquire operation and a release
    // operation, plus a single total order exists in which all threads observe all modifications in
    // the same order

    std::atomic<bool> x = {false};
    std::atomic<bool> y = {false};
    std::atomic<int> z = {0};

    {
        std::jthread write_x([&]() {
            //
            x.store(true, std::memory_order_seq_cst);
        });
        std::jthread write_y([&]() {
            y.store(true, std::memory_order_seq_cst);
        });
        std::jthread read_x_then_y([&]() {
            while (!x.load(std::memory_order_seq_cst))
                ;
            if (y.load(std::memory_order_seq_cst)) {
                ++z;
            }
        });
        std::jthread read_y_then_x([&]() {
            while (!y.load(std::memory_order_seq_cst))
                ;
            if (x.load(std::memory_order_seq_cst)) {
                ++z;
            }
        });
    }
    assert(z.load() != 0);
}

int main() {
    test_atomic_relaxed();
    test_consume_release();
    test_acquire_release_1();
    test_acquire_release_2();
    test_seq_cst();
    return 0;
}
 
Back
Top