Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does std::atomic access serve as a memory barrier?

Can the compiler reorder instructions on atomics, or do atomics serve as a memory barrier? Put again, can instructions written after an atomic instruction execute before the atomic instruction?

See the following code. If useMapA = false is moved before mapB is updated and the reading thread begins, we will use an invalid mapB.

Note: the update thread only happens once every 15 minutes, so we have a very well structured flow, and a way to avoid using an expensive lock call!

std::atomic<bool> useMapA;
std::map<string, string> mapA, mapB;

public void updateMap(map<string, string>* latestMap) {
    if (useMapA) {
        mapB = std::move(*latestMap);
        useMapA = false;
    } else {
        mapA = std::move(*latestMap);
        useMapA = true;
    }
}

inline map<string, string>& getMap() {
    return useMapA ? mapA : mapB;
}

Edit: I am interested in trading away being 100% thread-safe for speed (time = money). This read function needs to run really fast. You can assume that 15 minutes is long enough to avoid race conditions that would be caused if this time was much shorter.

like image 957
user221237 Avatar asked Apr 11 '14 01:04

user221237


People also ask

What is atomic_ thread_ fence?

atomic_thread_fence(std::memory_order_acquire) prevents, that a read operation before an acquire fence can be reordered with a reading or write operation after the acquire fence.

What is Compiler fence?

1) A compiler fence (by itself, without a CPU fence) is only useful in two situations: To enforce memory order constraints between a single thread and asynchronous interrupt handler bound to that same thread (such as a signal handler).


1 Answers

Before answering your question, I would like to show, how you can easily implement the feature with std::shared_ptr and atomic operations. The following implementation is efficient and thread-safe. There is also no need for the readers to create a copy of the map.

using string_map = std::map<std::string, std::string>;

std::shared_ptr<const string_map> map;

std::shared_ptr<const string_map> getMap()
{
    return std::atomic_load(&map);
}

void updateMap(string_map latestMap)
{
    std::shared_ptr<const string_map> temp{
        new string_map{std::move(latestMap)}};
    std::atomic_store(&map, temp);
}

Now, let's take a look at your code. It's a bit more complicated. To make it easier, let's assume that updateMap is called every second instead of every 15 minutes. useMapA is initially true. The update thread executes the following statements and will be interrupted before updating the atomic flag:

if (useMapA) {
    mapB = std::move(*latestMap);

Now, a reader thread only evaluates the atomic flag:

bool r1 = useMapA; // r1==true

The update thread is continued and sets the atomic flag to false. A second later, the update thread evaluates the atomic flag:

if (useMapA) { // condition is false

Now, the reader thread is continued. Both threads access mapA, and at least one thread writes to the data structure. That means, there is a data race, and that means the behavior of the program is undefined, regardless whether this data race really occurs.

What changes if the updateMap is called only every 15 minutes? Unless there happens some additional synchronization within this 15 minutes, it is still a data race, because the C++ Standard doesn't make a difference between a second and 15 minutes.

like image 174
nosid Avatar answered Sep 29 '22 10:09

nosid