c++multithreadingconcurrencystdthread

C++ Concurrency - memory_order_acquire


#include <atomic>
#include <thread>
std::vector<int> queue_data;
std::atomic<int> count;

void populate_queue()
{
    unsigned const number_of_items=20;
    queue_data.clear();
    for(unsigned i=0;i<number_of_items;++i)
    {
        queue_data.push_back(i);
    }
    count.store(number_of_items,std::memory_order_release);
}

void consume_queue_items()
{
    while(true)
    {
        int item_index;
        if((item_index=count.fetch_sub(1,std::memory_order_acquire))<=0)
        {
            wait_for_more_items();
            continue;
        }
        process(queue_data[item_index-1]);
    }
}

int main() {
    std::thread a(populate_queue);
    std::thread b(consume_queue_items);
    std::thread c(consume_queue_items);
    a.join();
    b.join();
    c.join(); 
}

This is a code snippet from book C++ concurrency in action, section 5.3.4.

Author says that

Without the release sequence rule or memory_order_release on the fetch_sub operations, there would be nothing to require that the stores to the queue_data were visible to the second consumer, and you would have a data race.

I don't understand why do we need to release on fetch_sub to synchronize data stored (ofcourse we need to synchronize count to avoid duplicate processing) but why doesn't 2nd consumer synchronize with release of populate_queue thread?


Solution

  • I don't understand why do we need to release on fetch_sub

    You don't. Notice that the code uses an acquire and says that this is okay.

    why doesn't 2nd consumer synchronize with release of populate_queue thread?

    It does.

    Notice what your quote from the book says (emphasis mine):

    Without the release sequence rule or memory_order_release on the fetch_sub operations,

    What does the release sequence rule say, quoted from the standard:

    A release sequence headed by a release operation A on an atomic object M is a maximal contiguous sub-sequence of side effects in the modification order of M , where the first operation is A, and every subsequent operation is an atomic read-modify-write operation

    Therefore you have a release sequence of the form producer -> consumer 1 -> consumer 2 -> … (or consumer 2 -> consumer 1, if consumer 2 wins the race). The point is that consumer 1 overwriting the value of count does not break the release sequence and therefore consumer 2 still synchronizes with the producer, as explained here in the standard:

    An atomic operation A that performs a release operation on an atomic object M synchronizes with an atomic operation B that performs an acquire operation on M and takes its value from any side effect in the release sequence headed by A.

    So the second consumer still synchronizes with the producer even though it takes its value from the first consumer.