Search code examples
c++multithreadingscheduling

How to create a generic C++ class that takes in method of another class and runs it at given interval in a thread


My knowledge of C++ is basic and would appreciate some help in architecting my code in a scalable way. I have couple of classes with following general format

class Class1{
    public:
        Class1();
        ~Class1();
        run();
    private:
        var1;
        var2;
          .
          .

};

class Class2{
    public:
        Class2();
        ~Class2();
        run(args1, args2);
    private:
        var1;
        var2;
          .
          .

};

class Class3{
    public:
        Class3();
        ~Class3();
        run(args1);
    private:
        var1;
        var2;
          .
          .

};

Now I would like to create a generic C++ class which takes in a function handle from above classes, spawns a thread, sets CPU affinity, sets sched_priority and runs that function at an user provided interval. So something like below

class GenericScheduler{
    public:
        GenericScheduler();
        ~GenericScheduler(Stop the thread and clean up);
        CreateAndRun(function, interval, cpu_2_use, sched_priority){
            Step1. Create a new thread
            Step2. Set the new thread to run on **cpu_2_use** at **sched_priority** level
            Step3. Run the **function** in a loop indefinitely at the provided **interval**
        }
 
};

Then the main would look like below

int main (int argc, char *argv[]){
    Class1 Class1Task;
    Class2 Class2Task;
    Class3 Class3Task;
    
    GenericScheduler scheduler1;
    GenericScheduler scheduler2;
    GenericScheduler scheduler3;

    scheduler1.CreateAndRun(Class1Task.run(), 10ms, cpu = 1, sched_priority = 5);
    scheduler2.CreateAndRun(Class2Task.run(args1, args2), 20ms, cpu = 2, sched_priority = 10);
    scheduler3.CreateAndRun(Class3Task.run(args1), 5ms, cpu = 1, sched_priority = 20);
}

Is it possible to have this kind of flexibility? Also as a follow up question can we do this with only one GenericScheduler object instead of 3 (that is instead of using scheduler1, scheduler2, scheduler3 only use one scheduler)? Looking forward to what people suggest because I can't seem to come up with anything.


Solution

  • This is the kind of scheduler skeleton code I would write. To create the loop timeout never use sleep, condition variables are great to get more responsive code.

    Demo here : https://onlinegdb.com/Q48nmDbdj

    Use m_thread_ptr->native_handle(); to get the native handle of the thread to be able use OS specicific calls (pthread/winapi) to change thread priorities etc.

    #include <chrono>
    #include <thread>
    #include <condition_variable>
    #include <mutex>
    #include <iostream>
    
    using namespace std::chrono_literals;
    
    class scheduler_t
    {
    public:
    
        // In multithreading context always take extra care to
        // shutdown nicely e.g. cooperatively
        ~scheduler_t()
        {
            if (m_thread_ptr != nullptr)
            {
                set_thread_state(thread_state_v::stopping); // signal workerthread to finish
                wait_for_thread_state(thread_state_v::stopped); 
                m_thread_ptr->join(); // join for correct shutdown without race conditions
            }
        }
    
        template<typename fn_t>
        void schedule_and_run(const std::chrono::steady_clock::duration interval, fn_t fn) // TODO thread priority etc...
        {
            m_thread_ptr = std::make_unique<std::thread>([=]
            {
                set_thread_state(thread_state_v::running);
    
                while (true)
                {
                    fn();
    
                    std::unique_lock<std::mutex> lock{m_mtx};
                    // Use condition variables wait with timeout to
                    // have a "sleep" that can respond immediately on shutdown
                    if (m_cv.wait_for(lock, interval, [this] { return m_state == thread_state_v::stopping; }))
                    {
                        break;
                    }
                }
    
                set_thread_state(thread_state_v::stopped);
            });
    
            // do not return until scheduled thread has really started running
            // avoids some race conditions in more complex code
            wait_for_thread_state(thread_state_v::running);
        }
    
    private:
    
        // from this codes point of view the scheduled thread has some states 
        enum class thread_state_v { idle, running, stopping, stopped };
    
        // allow for inter-thread synchronization based on state of the worker thread
        void set_thread_state(thread_state_v state)
        {
            std::unique_lock<std::mutex> lock{m_mtx};
            m_state = state;
            m_cv.notify_all();
        }
    
        void wait_for_thread_state(thread_state_v state)
        {
            std::unique_lock<std::mutex> lock{m_mtx};
            m_cv.wait(lock, [&] { return m_state == state; });
        }
    
        std::mutex m_mtx;
        std::condition_variable m_cv;
        thread_state_v m_state{ thread_state_v::idle };
        std::unique_ptr<std::thread> m_thread_ptr;
    };
    
    
    int main()
    {
        scheduler_t scheduler;
        scheduler.schedule_and_run(100ms, [] {std::cout << "."; });
        std::this_thread::sleep_for(1s);
        return 0;
    }