Взаимоблокировка потока C ++ при условии ожидания

Попытка расширить в своих двух предыдущих вопросах Операции перемещения для класса с потоком в качестве переменной-члена и Вызов функции внутри лямбды, переданной в поток

Я не понимаю, почему поток, выполняющий wait_for, иногда не получает уведомления, что приводит к тупиковой ситуации. Cppreference говорит о переменных состояния http://en.cppreference.com/w/cpp/thread/condition_variable/notify_one

Уведомляющему потоку не нужно удерживать блокировку на том же мьютексе, что и у ожидающего (ых) потока (ов); на самом деле это пессимизация, поскольку уведомленный поток немедленно снова заблокируется, ожидая, пока уведомляющий поток снимет блокировку.

MCVE, прокомментированная строка объясняет, что меняется, если я удерживаю блокировку, но я не понимаю, почему:

#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>

#include <iostream>

using namespace std;

class worker {
public:
    template <class Fn, class... Args>
    explicit worker(Fn func, Args... args) {
        t = std::thread(
            [&func, this](Args... cargs) -> void {
                std::unique_lock<std::mutex> lock(mtx);
                while (true) {
                    cond.wait(lock, [this]() -> bool { return ready; });
                    if (terminate) {
                        break;
                    }

                    func(cargs...);
                    ready = false;
                }
            },
            std::move(args)...);
    }

    ~worker() {
        terminate = true;
        if (t.joinable()) {
            run_once();
            t.join();
        }
    }

    void run_once() {
        // If i dont hold this mutex the thread is never notified of ready being
        // true.
        std::unique_lock<std::mutex> lock(mtx);
        ready = true;
        cout << "ready run once " << ready << endl;
        cond.notify_all();
    }

    bool done() { return (!ready.load()); }

private:
    std::thread t;
    std::atomic<bool> terminate{false};
    std::atomic<bool> ready{false};
    std::mutex mtx;
    std::condition_variable cond;
};

// main.cpp

void foo() {
    worker t([]() -> void { cout << "Bark" << endl; });
    t.run_once();
    while (!t.done()) {
    }
}

int main() {
    while (true) {
        foo();
    }
    return 0;
}

person aram    schedule 28.07.2016    source источник
comment
Готово = правда: это должно произойти под мьютексом. Неважно, что это атомарно.   -  person Cubbi    schedule 28.07.2016
comment
@ user2079303 Исправил! По-прежнему трудно увидеть, истечет ли срок исполнения.   -  person aram    schedule 28.07.2016
comment
@ user2079303 Это не мгновенный тупик, по крайней мере, на моем компьютере. Вот почему я поставил цикл, чтобы можно было вызывать функцию несколько раз.   -  person aram    schedule 28.07.2016
comment
@ user2079303 Да, очень вводит в заблуждение, опять же мой плохой. Извините. Я исправил это на случай, если у кого-то такая же проблема.   -  person aram    schedule 28.07.2016


Ответы (1)


Вам нужен барьер памяти, чтобы гарантировать, что другой поток увидит измененное значение «готово». "ready", будучи атомарным, гарантирует только то, что доступ к памяти упорядочен так, что изменения, произошедшие до атомарного доступа, фактически сбрасываются в основную память. Это не гарантирует, что другие потоки увидят эту память, поскольку эти потоки могут иметь свой собственный кэш памяти. Следовательно, чтобы гарантировать, что другой поток видит «готовую» модификацию, мьютекс необходим.

{
  std::unique_lock<std::mutex> lock(mtx);
  ready = true;
}
person Sven Nilsson    schedule 28.07.2016
comment
Метод load для std::atomic имеет параметр для определения видимости атомарных значений в потоках. Из документации для метода load: Если атомарное хранилище в потоке A помечено memory_order_release, а атомарная загрузка в потоке B из той же переменной помечена memory_order_acquire, все записи в память (неатомарные и расслабленные атомарные), которые произошли до атомарного store с точки зрения потока A, становятся видимыми побочные эффекты в потоке B, то есть после завершения атомарной загрузки поток B гарантированно видит все, что поток A записал в память. - person keith; 28.07.2016
comment
Рассуждения в ответе неверны. Как указывает Кейт, атомарность гарантирует, что запись будет действительно видима в другом потоке. Проблема не в кешировании, а в гонке с порядком планирования различных потоков. Решение правильное. - person eerorika; 28.07.2016