Код-ревью Senior Многопоточность и модель памяти обновлено
Ревью: очередь задач и рабочий поток
Вопрос
Коллега написал очередь задач для фонового потока. Найдите проблемы синхронизации и времени жизни.
class Worker {
public:
void start() {
std::thread([this] { loop(); }).detach();
}
void post(std::function<void()> task) {
queue_.push(std::move(task));
cv_.notify_one();
}
void stop() { stopped_ = true; cv_.notify_all(); }
int processed() const { return processed_; }
private:
void loop() {
while (!stopped_) {
std::unique_lock lock(m_);
cv_.wait(lock);
auto task = std::move(queue_.front());
queue_.pop();
task();
++processed_;
}
}
std::queue<std::function<void()>> queue_;
std::mutex m_;
std::condition_variable cv_;
bool stopped_ = false;
int processed_ = 0;
};
void transfer(Account& a, Account& b, int x) {
std::lock_guard l1(a.m);
std::lock_guard l2(b.m);
a.balance -= x;
b.balance += x;
}
Коротко
Очередь задач классическая точка гонок: все обращения к очереди и флагу остановки должны быть под одним мьютексом, ожидание только с предикатом cv.wait(lock, [&] { return stopped !queue.empty(); }), а задачу нужно извлечь под замком и выполнить уже без него.
Что должен найти ревьюер
- Ошибкастрока 4detach с захватом this: если Worker уничтожат, поток продолжит обращаться к его полям. Нужен std::jthread или join в деструкторе.
- Ошибкастрока 7queue_.push без мьютекса, а loop читает очередь под мьютексом: гонка данных.
- Ошибкастрока 10stopped_ обычный bool, читается в другом потоке без синхронизации: гонка. Нужен atomic или запись под мьютексом.
- Ошибкастрока 11processed_ читается без синхронизации, пока поток его меняет: гонка данных.
- Ошибкастрока 16wait без предиката: ложное пробуждение или notify до начала ожидания приведут к front() на пустой очереди (UB) или к вечному сну.
- Производительностьстрока 19Задача выполняется под мьютексом: post блокируется на всё время выполнения, а задача, вызвавшая post, получит дедлок.
- Ошибкастрока 31Мьютексы захватываются в порядке аргументов: transfer(a, b) и transfer(b, a) в разных потоках дадут дедлок. Нужен std::scoped_lock(a.m, b.m).
Разбор
Очередь задач классическая точка гонок: все обращения к очереди и флагу остановки должны быть под одним мьютексом, ожидание только с предикатом cv_.wait(lock, [&] { return stopped_ || !queue_.empty(); }), а задачу нужно извлечь под замком и выполнить уже без него. Время жизни потока привязывают к объекту через std::jthread или join в деструкторе. Для двух мьютексов std::scoped_lock.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.
Ещё по теме «Многопоточность и модель памяти»
- Найти потенциальный дедлок по порядку захвата мьютексов
- Пул потоков: кто и когда выполнит задачу
- Публикация данных через флаг
- Ловушка std::async
- Потокобезопасная ленивая инициализация
- compare_exchange
- Ожидание на condition_variable
- Что такое гонка данных