Can std::atomic memory barriers be used to transfer non-atomic data between threads?
Asked Answered
L

1

10

Is the following code standards compliant? (or can it be made compliant without making x atomic or volatile?)

This is similar to an earlier question, however I would like a citation to the relevant section of the C++ standard, please.

My concern is that atomic store() and load() do not provide sufficient compiler barriers for the non-atomic variables (x in the example below) to have correct release and acquire semantics.

My goal is to implement lock-free primitives, such as queues, that can transfer pointers to regular C++ data structures between threads.

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

int x; // regular variable, could be a complex data structure

std::atomic<int> flag { 0 };

void writer_thread() {
    x = 42;
    // release value x to reader thread
    flag.store(1, std::memory_order_release);
}

bool poll() {
    return (flag.load(std::memory_order_acquire) == 1);
}

int main() {
    x = 0;

    std::thread t(writer_thread);

    // "reader thread" ...  
    // sleep-wait is just for the test.
    // production code calls poll() at specific points

    while (!poll())
      std::this_thread::sleep_for(std::chrono::milliseconds(50));

    std::cout << x << std::endl;

    t.join();
}
Llamas answered 7/2, 2016 at 13:37 Comment(1)
I don't see any problems here. Can you go into more detail about what you suppose is unsafe?Thriftless
S
8

With acquire/release, yes, this will be enough. Relevant quotes (from cppreference—as good as the standard in most cases):

Memory Model

When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless either

  • both conflicting evaluations are atomic operations (see std::atomic)
  • one of the conflicting evaluations happens-before another (see std::memory_order)

std::memory_order

Release-Acquire ordering

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.

Schaumberger answered 7/2, 2016 at 13:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.