Избегайте ложных пробуждений в условной переменной, уведомляющих о неизвестном количестве потоков - PullRequest
0 голосов
/ 01 апреля 2020

Я использую условные переменные для периодического уведомления неизвестного количества потоков. то есть al oop будет периодически изменять значение и затем уведомлять все потоки, ожидающие переменную. Обычный способ избежать ложных пробуждений - использовать логическую переменную.

Поток, ожидающий переменную после пробуждения, устанавливает значение bool равным false

class foo
{
    std::mutex mtx;
    std::condition_variable cv;
    bool update;
    public:

    void loop()
    {
        std::this_thread::seep_for(std::chrono::milliseconds(100));
        {
            std::lock_guard<std::mutex> lock(mtx);
            update = true;
        }
        cv.notify_all();
    }

    void wait()
    {
        while(!update)
        {
            std::unique_lock<std::mutex> lock(mtx);
            cv.wait();
            if(update)
            {
                update = false;
                break;
            }
        }
    }

};

Функция foo::loop запускается в потоке, и другие потоки, выполняющие процедуры periodi c, будут ожидать через функцию foo::wait.

Но поскольку разные потоки могут начинать ожидать в разное время может показаться, что каждому потоку потребовалась бы специальная логическая переменная, чтобы гарантировать отсутствие ложных пробуждений.

Поскольку я не знаю, сколько потоков может ожидать его, я не могу использовать массивы bool или что-то еще.

Так есть ли другой способ, кроме использования std::map<std::thread::id,bool> для отслеживания всех обновлений.

1 Ответ

0 голосов
/ 01 апреля 2020

Используя счетчик циклов, вы можете убедиться, что все потоки включаются в уведомлении, и нет никаких ложных срабатываний. Единственная проблема заключается в возможности переполнения счетчика циклов.

Примечание. Я также переместил предикат cv.wait() в лямбду.

class foo
{
    std::mutex mtx;
    std::condition_variable cv;
    int cycle_count = 0;
    public:

    void loop()
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        {
            std::lock_guard<std::mutex> lock(mtx);
            ++cycle_count;
        }
        cv.notify_all();
    }

    void wait()
    {
        std::unique_lock<std::mutex> lock(mtx);
        int expected_cycle = cycle_count + 1;
        cv.wait(lock, [expected_cycle](){ return cycle_count >= expected_cycle; });
    }

};

Если все в порядке, то поток контроллера ожидает всем рабочим перед началом обновления счетчик циклов можно заменить на bool, который переворачивается взад и вперед, что позволяет избежать проблемы переполнения.

class foo
{
    std::mutex mtx;
    std::condition_variable cv;
    bool is_cycle_count_even = true;
    int threads_waiting;
    int threads_to_notify;
    public:

    void loop()
    {
        while (true) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::lock_guard<std::mutex> lock(mtx);
            if (threads_to_notify == 0) { break; }
        }

        {
            std::lock_guard<std::mutex> lock(mtx);
            threads_to_notify = threads_waiting;
            threads_waiting = 0;
            is_cycle_count_even ^= true;
        }
        cv.notify_all();
    }

    void wait()
    {
        std::unique_lock<std::mutex> lock(mtx);
        ++threads_waiting;
        bool expected_cycle = !is_cycle_count_even;
        cv.wait(lock, [expected_cycle](){ return is_cycle_count_even == expected_cycle; });
        --threads_to_notify;
    }

};

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

...