Search code examples
c++multithreadingconditional-variable

Synchronize three Threads in C++ one after the other


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

Solution

  • Using Semaphores would a bit more straight forward for this application. The following program outputs your possible output #1:

    Compiled with g++ 12.2.1 with command line: g++ -std=c++20 -pthread MultiClass.cpp

    Edit: As others have pointed out, doing this single threaded would be better.

    MultiClass.cpp

    #include<thread>
    #include<mutex>
    #include<iostream>
    #include<semaphore>
    
    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:
        std::binary_semaphore t1{1};
        std::binary_semaphore t2{0};
        std::binary_semaphore t3{0};
    
        void Calc1() {
            t1.acquire();
            for (int i = 0; i < 10; ++i) {
                std::cout << "T1:" << i << std::endl;
            }
            t2.release();
        }
        void Calc2() {
            t2.acquire();
            for (int i = 0; i < 10; ++i) {
                std::cout << "T2:" << i << std::endl;
            }
            t3.release();
        }
        void Calc3() {
            t3.acquire();
            for (int i = 0; i < 10; ++i) {
                std::cout << "T3:" << i << std::endl;
            }
        }
    };
    
    
    int main() {
        MultiClass m;
        m.Run();
        return 0;
    }