463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 123450

Correct usage of std::condition_variable to trigger timed execution

I am trying to execute a piece of code in fixed time intervals. I have something based on naked pthread and now I want to do the same using std::thread.

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

bool running;
std::mutex mutex;
std::condition_variable cond;

void timer(){
  while(running) {
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::lock_guard<std::mutex> guard(mutex);
    cond.notify_one();
  }
  cond.notify_one();
}

void worker(){
  while(running){
    std::unique_lock<std::mutex> mlock(mutex);
    cond.wait(mlock);
    std::cout << "Hello World" << std::endl;
    //... do something that takes a variable amount of time ...//
  }
}

int main(){
  running = true;
  auto t_work = std::thread(worker);
  auto t_time = std::thread(timer);
  std::this_thread::sleep_for(std::chrono::milliseconds(10000));

  running = false;
  t_time.join();
  t_work.join();
}

The worker in reality does something that takes a variable amount of time, but it should be scheduled at fixed intervals. It seems to work, but I am pretty new to this, so some things arent clear to me...

PS: I know that there are other ways to ensure a fixed time interval, and I know that there are some problems with my current approach (eg if worker needs more time than the interval used by the timer). However, I would like to first understand that piece of code, before changing it too much.

Upvotes: 2

Views: 3347

Answers (2)

ovanes
ovanes

Reputation: 5673

Let's first check the background concepts.

Critical Section

First of all Mutex is needed to mutually exclude access to a critical section. Usually, critical section is considered to be shared resource. E.g. a Queue, Some I/O (e.g. socket) etc. In plain words Mutex is used to guard shared resource agains a Race Condition, which can bring a resource into undefined state.

Example: Producer / Consumer Problem

A queue should contain some work items to be done. There might be multiple threads which put some work items into the Queue (i.e. produce items => Producer Threads) and multiple threads which consume these items and do smth. useful with them (=> Consumer Threads).

Put and Consume operations modify the Queue (especially its storage and internal representations). Thus when running either put or consume operations we want to exclude other operations from doing the same. This is where Mutex comes into play. In a very basic constellation only one thread (no matter producer or consumer) can get access to the Mutex, i.e. lock it. There exist some other Higher Level locking primitives to increase throughput dependent on usage scenarios (e.g. ReaderWriter Locks)

Concept of Condition Variables

condition_variable::notify_one wakes up one currently waiting thread. At least one thread has to wait on this variable:

  • If no threads are waiting on this variable posted event will be lost.
  • If there was a waiting thread it will wake up and start running as soon as it can lock the mutex associated with the condition variable. So if the thread which initiated the notify_one or notify_all call does not give up the mutex lock (e.g. mutex::unlock() or condition_variable::wait()) woken up thread(s) will not run.

In the timer() thread mutex is unlocked after notify_one() call, because the scope ends and guard object is destroyed (destructor calls implicitly mutex::unlock())

Problems with this approach

Cancellation and Variable Caching

Compilers are allowed to cache values of the variables. Thus setting running to true might not work, as the values of the variable might be cached. To avoid that, you need to declare running as volatile or std::atomic<bool>.

worker Thread

You point out that worker needs to run in some time intervals and it might run for various amounts of time. The timer thread can only run after worker thread finished. Why do you need another thread at that point to measure time? These two threads always run as one linear chunk and have no critical section! Why not just put after the task execution the desired sleep call and start running as soon as time elapsed? As it turns out only std::cout is a shared resource. But currently it is used from one thread. Otherwise, you'd need a mutex (without condition variable) to guard writes to cout only.

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

std::atomic_bool running = false;

void worker(){
  while(running){
    auto start_point = std::chrono::system_clock::now();
    std::cout << "Hello World" << std::endl;
    //... do something that takes a variable amount of time ...//

    std::this_thread::sleep_until(start_point+std::chrono::milliseconds(1000));

  }
}

int main(){
  running = true;
  auto t_work = std::thread(worker);
  std::this_thread::sleep_for(std::chrono::milliseconds(10000));

  running = false;
  t_work.join();
}

Note: With sleep_until call in the worker thread the execution is blocked if your task was blocking longer than 1000ms from the start_point.

Upvotes: 2

ACB
ACB

Reputation: 1637

Why do I need a mutex at all? I do not really use a condition, but whenever the timer sends a signal, the worker should do its job.

The reason you need a mutex is that the thread waiting for the condition to be satisfied could be subject to a spurious wakeup. To make sure your thread actually received the notification that the condition is correctly satisfied you need to check that and should do so with a lambda inside the wait call. And to guarantee that the variable is not modified after the spurious wakeup but before you check the variable you need to acquire a mutex such that your thread is the only one that can modify the condition. In your case that means you need to add a means for the worker thread to actually verify that the timer did run out.

Does the timer really need to call cond.notify_one() again after the loop? This was taken from the older code and iirc the reasoning is to prevent the worker to wait forever, in case the timer finishes while the worker is still waiting.

If you dont call notify after the loop the worker thread will wait indefinitely. So to cleanly exit your program you should actually call notify_all() to make sure every thread waiting for the condition variable wakes up and can terminate cleanly.

Do I need the running flag, or is there a nicer way to break out of the loops?

A running flag is the cleanest way to accomplish what you want.

Upvotes: 2

Related Questions