Код-ревью 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(); }), а задачу нужно извлечь под замком и выполнить уже без него.

Что должен найти ревьюер

  1. Ошибкастрока 4
    detach с захватом this: если Worker уничтожат, поток продолжит обращаться к его полям. Нужен std::jthread или join в деструкторе.
  2. Ошибкастрока 7
    queue_.push без мьютекса, а loop читает очередь под мьютексом: гонка данных.
  3. Ошибкастрока 10
    stopped_ обычный bool, читается в другом потоке без синхронизации: гонка. Нужен atomic или запись под мьютексом.
  4. Ошибкастрока 11
    processed_ читается без синхронизации, пока поток его меняет: гонка данных.
  5. Ошибкастрока 16
    wait без предиката: ложное пробуждение или notify до начала ожидания приведут к front() на пустой очереди (UB) или к вечному сну.
  6. Производительностьстрока 19
    Задача выполняется под мьютексом: post блокируется на всё время выполнения, а задача, вызвавшая post, получит дедлок.
  7. Ошибкастрока 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 ошибок.