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. |