-1

I was working on one of the examples from the book Concurrency With Modern C++ by Rainer Grimm and I found this code snippet (edited somewhat to get better undestanding of the concept). The code snippet is related to top Release Sequence

#include <atomic>
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std::chrono_literals;
int work{0};
std::atomic<int> ready{0};

void consumer() {
    while(!(ready.fetch_sub(1, std::memory_order_acquire) > 0)) {
       std::this_thread::sleep_for(100ms); 
        }
        work--;
    std::cout<<"soemthing is here" << work <<std::endl;
}

void producer() {
    work = 2019;
    ready.store(2, std::memory_order_release);

}

int main () {
    std::thread prod(producer);
    std::thread con(consumer);
    std::thread con2(consumer);

    prod.join();
    con.join();
    con2.join();
    return 0;
}

The output of the above program comes out to be
soemthing is here2018\nsoemthing is here2017\n
soemthing is heresoemthing is here20172017\n\n
soemthing is here2018soemthing is here2017\n\n
soemthing is heresoemthing is here20172017\n\n

Can anyone explain why there is so much difference in the output and how this output can be justified using acquire-release semantics and release sequence.
Note: \n is used to represent output of std::endl;

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Piyush Vijay
  • 165
  • 9
  • 2
    What, exactly you are asking about? Both the usage of `std::cout`, and `work--;` are unsynchronized modification. Due to this, your code exhibits undefined behavior, and any reasoning about the specific behavior of undefined behavior is pointless. – Algirdas Preidžius Nov 20 '19 at 21:16
  • @AlgirdasPreidžius How the synchronisation can be done between `std::cout` and `work--` without using lock here. And also how the line `std::cout<<"soemthing is here" << work < – Piyush Vijay Nov 21 '19 at 05:00
  • 1
    1) "_How the synchronisation can be done between `std::cout` and `work--` without using lock here._" You need to have a lock, so that the object is accessed only from a single thread at a time. 2) "_And also how the line `std::cout<<"soemthing is here" << work < – Algirdas Preidžius Nov 21 '19 at 14:03
  • @PiyushVijay Why would use iostream w/o locking? – curiousguy Nov 21 '19 at 17:08

1 Answers1

0

Your code has UB because work isn't atomic<int>. But your producer and both consumers write it without synchronization. The symptom in this case is that the compiler optimized consumer to keep its own local copy of work in a register so you get numbers repeated, once from each thread.

And of course you need to use int tmp = work--; to do one fetch_sub, instead of an atomic decrement and then later reading a value that might have changed.


The other showstopper is that three separate cout calls for the three separate << operators are not a single atomic print.

cout itself is thread-safe, though.

You could print into a full line (including \n) into a private string or char buffer and then safely cout that. std::endl isn't magic; it's literally identical to << '\n' and flushing the stream (in case it was full-buffered so \n didn't already do that).

e.g. sprintf or stringstream or whatever.

That would make sure that whole lines like soemthing is here2018\n are always contiguous. (You probably want to spellcheck and leave a trailing space).

That doesn't guarantee they'll print in numeric order, though. One consumer might decrement first, but the other thread decrements second but then gets the lock (inside std::cout) first to print a lower-numbered line first.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847