# 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 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).

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

CWE-820

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