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

  • Thread starter Thread starter ptf
  • Start date Start date

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 Bottom