C++ Multithreading – How to Synchronize Three Threads in C++ One After the Other

c++conditional-variablemultithreading

This is in continuation to this really nice one: https://stackoverflow.com/a/33500554/22598822. That post was for the sequence (t1 & t2) –> t3. Let's say I have a program running, and there are three threads that I want to keep in an inner execution sequence t1 –> t2 –> t3 (other threads may or may not be running simultaneously on the system). How can it be done?

I took the code from the reference above and tried to rework it for this goal, though unsuccessfully. My implementation is incorrect, I'm putting it here for a minimal reproducible example and maybe a possible starting point.

Header.h:

#include<thread>
#include<mutex>
#include<iostream>
#include <condition_variable>

MultiClass.h

#include "Header.h"
#include "SynchObj.h"

class MultiClass {
public:
    void Run() {
        std::thread t1(&MultiClass::Calc1, this);
        std::thread t2(&MultiClass::Calc2, this);
        std::thread t3(&MultiClass::Calc3, this);
        t1.join();
        t2.join();
        t3.join();
    }
private:
    SyncObj obj;
    void Calc1() {
        for (int i = 0; i < 10; ++i) {
            obj.waitForCompletionOfT3();
            std::cout << "T1:" << i << std::endl;
            obj.signalCompletionOfT1();
        }           
    }
    void Calc2() {
        for (int i = 0; i < 10; ++i) {
            obj.waitForCompletionOfT1();
            std::cout << "T2:" << i << std::endl;
            obj.signalCompletionOfT2();
        }
    }
    void Calc3() {      
        for (int i = 0; i < 10; ++i) {
            obj.waitForCompletionOfT2();
            std::cout << "T3:" << i << std::endl;
            obj.signalCompletionOfT3();
        }       
    }
};

SynchObj.h

#include "Header.h"

class SyncObj {
    std::mutex mux;
    std::condition_variable cv;  
    bool completed[3]{ false, false, false };

public:

    /***** Original (t1 & t2) --> t3 *****/
    /*
    void signalCompetionT1T2(int id) {
        std::lock_guard<std::mutex> ul(mux);
        completed[id] = true;
        cv.notify_all();
    }
    void signalCompetionT3() {
        std::lock_guard<std::mutex> ul(mux);
        completed[0] = false;
        completed[1] = false;
        cv.notify_all();
    }
    void waitForCompetionT1T2() {
        std::unique_lock<std::mutex> ul(mux);             
        cv.wait(ul, [&]() {return completed[0] && completed[1]; });         
    }
    void waitForCompetionT3(int id) {
        std::unique_lock<std::mutex> ul(mux);         
        cv.wait(ul, [&]() {return !completed[id]; });           
    }
    */       
    /***********************************/
    
    /*** Unsuccessful attempt at t1 --> t2 --> t3 ***/

    void signalCompletionOfT1() {
        std::lock_guard<std::mutex> ul(mux);
        completed[0] = true;
        cv.notify_all();
    }

    void signalCompletionOfT2() {
        std::lock_guard<std::mutex> ul(mux);
        completed[0] = false;
        completed[1] = true;
        cv.notify_all();
    }

    void signalCompletionOfT3() {
        std::lock_guard<std::mutex> ul(mux);
        completed[0] = false;
        completed[1] = false;
        completed[2] = true;
        cv.notify_all();
    }

   void waitForCompletionOfT1() {
        std::unique_lock<std::mutex> ul(mux);             
        cv.wait(ul, [&]() {return !completed[2]; });         
    }

    void waitForCompletionOfT2() {
        std::unique_lock<std::mutex> ul(mux);             
        cv.wait(ul, [&]() {return !completed[0]; });         
    }

    void waitForCompletionOfT3() {
        std::unique_lock<std::mutex> ul(mux);         
        cv.wait(ul, [&]() {return !completed[1]; });           
    }

};

Source.cpp:

#include "Header.h"
#include "MultiClass.h"

int main() {
    MultiClass m;
    m.Run();
    return 0;
}

Possible output #1 (Desired):

T1:1
T1:2
T1:3
T1:4
T1:5
T1:6
T1:7
T1:8
T1:9
T2:0
T2:1
T2:2
T2:3
T2:4
T2:5
T2:6
T2:7
T2:8
T2:9
T3:0
T3:1
T3:2
T3:3
T3:4
T3:5
T3:6
T3:7
T3:8
T3:9

Possible output #2:

0
T2:1
T2:2
T2:3
T2:4
T2:5
T2:6
T2:7
T2:8
T2:9

Best Answer

This is an extended comment, not an answer.

Your SyncObj class could be simpler. A SyncObj instance has three possible states, which you represent by using three separate bool variables. In electrical engineering, that pattern is known as one-hot encoding. In software engineering (as far as I know) nobody's given it a name because hardly anybody ever uses it.

If an instance has three possible states, the simplest way to represent the state is with an int variable that can be set to 0, 1, or 2 or, with an enum variable that has three possible values. In your case, you always want the states to progress through the same sequence, so the numeric representation probably is more natural. Something like this, for example:

#include "Header.h"

constexpr int NUMPARTICIPANTS=3;

class SyncObj {
    std::mutex mux;
    std::condition_variable cv;  
    int whoseTurn=0;

public:

    void awaitMyTurn(int myID) {
        std::lock_guard<std::mutex> ul(mux);
        cv.wait(ul, [&]() {return whoseTurn == myID; });         
    }

    void signalNext() {
        std::lock_guard<std::mutex> ul(mux);
        whoseTurn = (whoseTurn + 1) % NUMPARTICIPANTS;
        cv.notify_all();
    }
};

Use it like this:

    void Calc2() {
        for (int i = 0; i < 10; ++i) {
            obj.awaitMyTurn(2);
            std::cout << "T2:" << i << std::endl;
            obj.signalNext();
        }
    }

You could fancy it up by making NUMPARTICIPANTS a constructor argument instead of making it a global constant, by throwing an error from awaitMyTurn if the given myID was out-of-bounds, etc.