>
>
>
V1089. Waiting on condition variable wi…


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

Диагностическое правило основано на пункте CP.42 CppCoreGuidelines.

Анализатор обнаружил ситуацию, в которой одна из нестатических функций-членов класса 'std::condition_variable' – 'wait', 'wait_for' или 'wait_until' – вызывается без предиката. Это может привести к проблемам: ложному пробуждению потока или его зависанию.

Рассмотрим пример N1, приводящий к потенциальному зависанию:

#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();
}

В примере есть состояние гонки. Программа может зависнуть, если она выполнится в следующем порядке:

  • поток 'p' выигрывает гонку, захватывает мьютекс первым, печатает сообщение в 'std::cout' и отпускает мьютекс;
  • поток 'c' захватывает мьютекс, но не успевает встать на ожидание через условную переменную 'cond';
  • поток 'p' оповещает о наступившем событии, отправляя уведомление через вызов 'cond.notify_one()';
  • поток 'c' встает на ожидание через условную переменную 'cond', ожидая нотификации.

Для исправления следует модифицировать код следующим образом:

  • Поток, который производит оповещение, должен изменить некоторое общее наблюдаемое состояние под блокировкой мьютекса. Например, булеву переменную.
  • Поток-обработчик должен вызвать перегрузку 'std::condition_variable::wait', принимающую предикат. Внутри него нужно проверить, произошло ли изменение общего состояния или нет.

Исправленный пример:

#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();
}

Рассмотрим пример N2, в котором может произойти ложное пробуждение:

#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();
}

Ложное пробуждение – явление, при котором ожидающий поток пробуждается и обнаруживает, что условие, которое он ожидал, не выполнено. Это может произойти в двух сценариях:

  • Оповещающий поток меняет общее состояние и отправляет нотификацию. Один поток-обработчик пробуждается, обрабатывает общее состояние и засыпает. Другой поток-обработчик также пробуждается от нотификации, но обнаруживает, что общее состояние уже обработано.
  • Ожидающий поток пробудился, даже если оповещающий поток ещё не отправил нотификацию. Такое может происходить в некоторых реализациях многопоточных API, например, WinAPI, POSIX Threads и др.

В примере N2 ложное пробуждение может произойти в потоках 'c1', 'c2' и 'c3'. В результате такого пробуждения очередь может оказаться пустой, и доступ к ней приведёт к неопределенному поведению.

Для исправления следует также вызвать перегрузку 'std::condition_variable::wait_for', принимающую предикат. Внутри него нужно проверить, пуста очередь или нет:

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);
  }
}

Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).

Данная диагностика классифицируется как:

Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1089.