Search code examples
c++multithreadingc++11pass-by-referenceatomic

C++ 11 can you safely pass and access std::atomics by reference in different threads


I am wondering if you can pass a an atomic by reference to a thread and for the .load and .store operations still be thread safe. For instance:

#include <thread>
#include <atomic>
#include <cstdlib>

void addLoop(std::atomic_int& adder)
{
    int i = adder.load();
    std::srand(std::time(0));
    while(i < 200)
    {
        i = adder.load();
        i += (i + (std::rand() % i));
        adder.store(i);
    }
}

void subLoop (std::atomic_int& subber)
{
    int j = subber.load();
    std::srand(std::time(0));
    while(j < 200)
    {
        j = subber.load();
        j -= (j - (std::rand() % j));
        subber.store(j);
    }
}

int main()
{
    std::atomic_int dummyInt(1);
    std::thread add(addLoop, std::ref(dummyInt));
    std::thread sub(subLoop, std::ref(dummyInt));
    add.join();
    sub.join();
    return 0;
}

When the addLoop thread stores the new value into the atomic if subLoop were to access it using the load and store functions would it end up being an undefined state?


Solution

  • According to [intro.races]/20.2,

    The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

    According to [intro.races]/2,

    Two expression evaluations conflict if one of them modifies a memory location (4.4) and the other one reads or modifies the same memory location.

    Accessing an atomic variable through a reference doesn't introduce any additional accesses or modifications, because references do not occupy memory locations. Therefore, performing potentially concurrent atomic operations is still safe when those operations occur through references.

    In fact, in the abstract model of evaluation in C++, there is no difference between accessing an object by its name and accessing an object through a reference variable bound to that object. Both are merely lvalues referring to the object.

    Beware of the std::atomic_init function, which is non-atomic:

    std::atomic<int> x;
    void f(std::atomic<int>& r) {
        std::atomic_init(&r, 0);
    }
    void g(std::atomic<int>& r) {
        r = 42;
    }
    

    In the above code, if f and g run in separate threads and both access the atomic variable x, a data race can occur, because one of the operations is not atomic. However, this is no different from triggering the data race like so:

    std::atomic<int> x;
    void f() {
        std::atomic_init(&x, 0);
    }
    void g() {
        x = 42;
    }
    

    where no references are involved.