>
>
>
V1089. Waiting on condition variable wi…


V1089. Waiting on condition variable without predicate. A thread can wait indefinitely or experience a spurious wake-up.

This diagnostic rule is based on the CP.42 CppCoreGuidelines.

The analyzer has detected one of the non-static member functions of the 'std::condition_variable' class template — 'wait', 'wait_for' or 'wait_until' — is called without a predicate. This can lead to a spurious wakeup of a thread or a thread hanging.

Let's consider the example N1 leading to a potential hanging:

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

std::mutex mtx;
std::condition_variable cond;

void consumer()
{
  std::unique_lock<std::mutex> lck { mtx };
  std::cout << "Waiting... " << std::endl;
  cond.wait(lck);                           // <=
  std::cout << "Working..." << std::endl;
}

void producer()
{
  {
    std::lock_guard<std::mutex> _ { mtx };
    std::cout << "Preparing..." << std::endl;
  }

  cond.notify_one();
}

int main() 
{
  std::thread c { consumer };
  std::thread p { producer };

  c.join();
  p.join();
}

The example contains a race condition. The program can hang if it runs in the following order:

  • the 'p' thread wins the race, acquires the mutex first, prints the message to the 'std::cout' and releases the mutex;
  • the 'c' thread acquires the mutex, but does not have time to block on the conditional variable 'cond';
  • the 'p' thread notifies about the event by calling the 'cond.notify_one()' call;
  • the 'c' thread waiting for notification blocks on the conditional variable 'cond'.

To fix this, we should modify the code as follows:

  • The notifying thread should acquire a mutex and change some shared state while the lock is held. For example, a Boolean variable.
  • The waiting thread should call the overload of 'std::condition_variable::wait' that accepts the predicate. Inside the predicate we need to check whether the shared state changed or not.

Here is the fixed example:

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

std::mutex mtx;
std::condition_variable cond;

bool pendingForWorking = false; // <=

void consumer()
{
  std::unique_lock<std::mutex> lck { mtx };
  std::cout << "Waiting... " << std::endl;
  
  cond.wait(lck, [] { return pendingForWorking; }); // <=
  std::cout << "Working..." << std::endl;
}

void producer()
{
  {
    std::lock_guard<std::mutex> _ { mtx };
    pendingForWorking = true;                 // <=
    std::cout << "Preparing..." << std::endl;
  }

  cond.notify_one();
}

int main() 
{
  std::thread c { consumer };
  std::thread p { producer };

  c.join();
  p.join();
}

Let's consider the example N2 where a spurious wakeup can happen:

#include <iostream>
#include <fstream>
#include <sstream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

std::queue<int> queue;
std::mutex mtx;
std::condition_variable cond;

void do_smth(int);

void consumer()
{
  while (true)
  {
    int var;

    {
      using namespace std::literals;
      std::unique_lock<std::mutex> lck { mtx };
      if (cond.wait_for(lck, 10s) == std::cv_status::timeout) // <=
      {
        break;
      }

      var = queue.front();
      queue.pop();
    }

    do_smth(var);
  }
}

void producer(std::istream &in)
{
  int var;
  while (in >> var)
  {
    {
      std::lock_guard<std::mutex> _ { mtx };
      queue.push(var);
    }

    cond.notify_one();
  }
}

void foo(std::ifstream &fin, std::istringstream &sin)
{
  std::thread p1 { &producer, std::ref(fin) };
  std::thread p2 { &producer, std::ref(sin) };
  std::thread p3 { &producer, std::ref(std::cin) };

  std::thread c1 { &consumer };
  std::thread c2 { &consumer };
  std::thread c3 { &consumer };

  p1.join(); p2.join(); p3.join();
  c1.join(); c2.join(); c3.join();
}

A spurious wakeup happens when a waiting thread wakes up and discovers that the condition it was expecting has not been met. This can occur in two scenarios:

  • The notifying thread changes the shared state and sends a notification. One thread wakes up, processes the shared state, and falls asleep. After that another thread also wakes up from the notification but finds that the shared state has already been processed.
  • The waiting thread is woken up even if the notifying thread has not yet sent a notification. This can happen in some implementations of multithreaded APIs, for example, WinAPI, POSIX Threads, etc.

In the example N2, a spurious wakeup can occur in threads 'c1', 'c2', and 'c3'. As a result of such a wakeup, the queue may be empty — accessing it, we can get an undefined behavior.

To fix this, we should also call the 'std::condition_variable::wait_for' overload that accepts the predicate. Inside the predicate, we need to check whether the queue is empty or not:

void consumer()
{
  while (true)
  {
    int var;

    {
      using namespace std::literals;
      std::unique_lock<std::mutex> lck { mtx };
      bool res = cond.wait_for(lck,
                               10s,
                               [] { return !queue.empty(); }); // <=
      if (!res)
      {
        break;
      }

      // no spurious wakeup
      var = queue.front();
      queue.pop();
    }

    do_smth(var);
  }
}

You can look at examples of errors detected by the V1089 diagnostic.