Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's are practical example where acquire release memory order differs from sequential consistency?

Clearly, sequential consistent atomic operations differ in their valid observable behavior from acquire-release only operations in a valid C++ program. Definitions are given in the C++ standard (since C++11) or here.

However, I have never come across a real-world example of an algorithm or a data structure that where acquire-release semantics is insufficient and sequential consistency is needed.

What would be an practical example of a real world algorithm or data structure, where sequential consistency is needed and acquire-release memory order is not enough?

Note, that even std::mutex does not guarantee sequential consistency.

like image 816
Ralph Tandetzky Avatar asked Jan 25 '17 18:01

Ralph Tandetzky


People also ask

What is memory sequential consistency?

Sequential consistency is a conservative memory model that does not allow any instruction reordering on each core. This prevents many optimizations and degrades performance. However, not all memory instructions on a single core need to preserve their program order.

What is Acquire release?

Acquire-release ordering guarantees that all memory operations which happen before the storing operation (in this case, y. store(true, std::memory_order_release) ) in one thread will be visible to the other thread that is doing the corresponding loading operation (likewise, y. load(std::memory_order_acquire) ).

What is sequential consistency C++?

The C++ memory model guarantees sequential consistency if you use atomic operations with the appropriate memory orderings to guarantee sequential consistency. If you just use plain non-atomic operations, or relaxed atomics, and no mutexes, then sequential consistency is not guaranteed.


1 Answers

Peterson's algorithm is an example of something that requires sequential consistency.

In the days before mutexes, the algoritm was used to give a single thread access to a protected area. The algoritm only works with 2 threads, each managing a flag that expresses the intention to access the protected area. If both set the flag at (about) the same time, both will backoff and try again. The real algorithm is more advanced since it uses a 'turn' flag to manage fair access, but to show the difference between seq/cst and acq/rel this is not necessary.

Below is a ready-to-compile, simplified version of the Peterson algorithm that actually shows that the algoritms is broken if something weaker than sequential consistency is used. Interestingly enough, this is broken even on X86 since that platform allows store-loads to be reordered.
The problem with store-load reordering is that both threads may express their intention to access the protected area by setting their me flag to true, while both are reading false from the him flag (since the value was not propagated yet to both threads) and enter the protected area. This is not possible with sequential consistency.

With gcc, I had to compile with -O3 optimizations to have the assert fire, with clang that was not necessary. Both compilers use a different approach to implement sequential consistency.

#include <thread>
#include <atomic>
#include <assert.h>

std::atomic<bool> flag1{false};
std::atomic<bool> flag2{false};

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

// Change these to memory_order_seq_cst to fix the algorithm
static const auto store_ordering = std::memory_order_release;
static const auto load_ordering  = std::memory_order_acquire;

void busy(int n)
{
    auto &me  = (n==1) ? flag1 : flag2;
    auto &him = (n==1) ? flag2 : flag1;

    for (;;)
    {
        for (;;)
        {
            me.store(true, store_ordering);
            if (him.load(load_ordering) == false)
            {
                // got the 'lock'
                break;
            }

            // retention, no wait period -> busy loop
            me.store(false, store_ordering);
        }

        int tmp = counter.fetch_add(1, std::memory_order_relaxed);
        assert(tmp == 0);

        /*
         * critical area
         */

        tmp = counter.fetch_sub(1, std::memory_order_relaxed);
        assert(tmp == 1);

        me.store(false, store_ordering);
    }
}


int main()
{
    std::thread t1{busy, 1};
    std::thread t2{busy, 2};

    t1.join();
    t2.join();
}
like image 98
LWimsey Avatar answered Oct 19 '22 03:10

LWimsey