Search code examples
c++multithreadingc++17thread-synchronization

How to wait for another thread to loop again?


In my C++17 application I have a thread that runs an endless loop, doing some work (taking some seconds each time) in each iteration.

Now I want to wait in another thread (or in multiple other threads) until the next loop iteration has finished.

Here is an example:

#include <atomic>
#include <chrono>
#include <thread>

void notify_any_waiting_threads()
{
    // TODO: how to implement this?
}

void wait_for_worker_thread_iteration()
{
    // TODO: how to implement this?
}


void do_work()
{
    std::this_thread::sleep_for(std::chrono::seconds(1)); // simulate some work
}

std::atomic_bool kill_worker_thread = false;
std::atomic_bool worker_should_be_idle = false;
void worker_thread_function()
{
    while (!kill_worker_thread) {

        if (worker_should_be_idle) {
            std::this_thread::sleep_for(std::chrono::seconds(1)); // sleep a bit, to avoid wasting CPU time
        } else {
            do_work();
        }

        notify_any_waiting_threads();
    }
}

void observer_thread_function()
{
    worker_should_be_idle = true;
    wait_for_worker_thread_iteration();
    // at this point I want to be sure that the worker thread does not execute do_work() any more
}

int main()
{
    std::thread worker_thread(worker_thread_function);

    std::this_thread::sleep_for(std::chrono::milliseconds(1800)); // simulate that observer thread does not start immediately
    std::thread observer_thread_1(observer_thread_function);

    // just some cleanup (not relevant for the question I think)
    observer_thread_1.join();
    kill_worker_thread = true;
    worker_thread.join();
}

What would be the best approach here to implement notify_any_waiting_threads() and wait_for_worker_thread_iteration()?

I was thinking of using a std::condition_variable; but then what would be the "condition" that is checked in the predicate given to std::condition_variable::wait()? And actually, am I allowed to notify the condition_variable in each iteration? Or is there some other synchronization primitive available that fits better?

Or is my idea of "wait for next loop iteration of another thread" a known anti-pattern?


Solution

  • you just need to use a std::condition_variable, that is notified when the iteration count changes.

    #include <atomic>
    #include <chrono>
    #include <thread>
    #include <condition_variable>
    #include <iostream>
    
    struct IterationCounter
    {
        std::condition_variable cv;
        int count = 0;
        std::mutex m;
        void wait()
        {
            std::unique_lock lk(m);
            auto old_value = count;
            cv.wait(lk, [&] { return old_value != count; });
        }
        void increment()
        {
            {
                std::unique_lock lk(m);
                count++;
            }
            cv.notify_all();
        }
    };
    
    void notify_any_waiting_threads(IterationCounter& counter)
    {
        counter.increment();
    }
    
    void wait_for_worker_thread_iteration(IterationCounter& counter)
    {
        counter.wait();
    }
    
    
    void do_work()
    {
        std::this_thread::sleep_for(std::chrono::seconds(1)); // simulate some work
    }
    
    std::atomic_bool kill_worker_thread = false;
    std::atomic_bool worker_should_be_idle = false;
    void worker_thread_function(IterationCounter& counter)
    {
        while (!kill_worker_thread) {
    
            if (worker_should_be_idle) {
                std::this_thread::sleep_for(std::chrono::seconds(1)); // sleep a bit, to avoid wasting CPU time
            }
            else {
                do_work();
            }
    
            notify_any_waiting_threads(counter);
        }
        std::cout << "stopped!\n";
    }
    
    
    void observer_thread_function(IterationCounter& counter)
    {
        worker_should_be_idle = true;
        wait_for_worker_thread_iteration(counter);
        std::cout << "notified!\n";
        kill_worker_thread = true;
    }
    
    int main()
    {
        IterationCounter counter;
        std::thread worker_thread(worker_thread_function, std::ref(counter));
    
        std::this_thread::sleep_for(std::chrono::milliseconds(1800)); // simulate that observer thread does not start immediately
        std::thread observer_thread_1(observer_thread_function, std::ref(counter));
    
        // just some cleanup (not relevant for the question I think)
        observer_thread_1.join();
        kill_worker_thread = true;
        worker_thread.join();
    }
    

    godbolt example

    Note that even if the int wraps around, it will still work fine, you might as well use a char for this, we don't care what its value is, only that it changes, and when it changes whoever was waiting on it will wake-up.

    For practical use you want to use std::shared_ptr<IterationCounter> to make the lifetime safer, it is not safe to store it inside either the main worker or the other workers without shared pointers, only the main thread will probably live long enough.