Безопасно ли это использование условной переменной (взято с cppreference.com)? - PullRequest
3 голосов
/ 08 апреля 2020

Этот код взят из Пример производителя / потребителя с условной переменной safe? У меня и коллеги возникли разногласия относительно того, безопасно ли пропуск std :: unique_lock lock (m) в close (). Линия done = true; устанавливается без блокировки. Может ли уведомление попасть в другой поток без синхронизации данных?

#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
#include <queue>
#include <chrono>

class condvarQueue
{
    std::queue<int> produced_nums;
    std::mutex m;
    std::condition_variable cond_var;
    bool done = false;
    bool notified = false;
public:
    void push(int i)
    {
        std::unique_lock<std::mutex> lock(m);
        produced_nums.push(i);
        notified = true;
        cond_var.notify_one();
    }

    template<typename Consumer>
    void consume(Consumer consumer)
    {
        std::unique_lock<std::mutex> lock(m);
        while (!done) {
            while (!notified) {  // loop to avoid spurious wakeups
                cond_var.wait(lock);
            }   
            while (!produced_nums.empty()) {
                consumer(produced_nums.front());
                produced_nums.pop();
            }   
            notified = false;
        }   
    }

    void close()
    {
        done = true;
        notified = true;
        cond_var.notify_one();
    }
};

int main()
{
    condvarQueue queue;

    std::thread producer([&]() {
        for (int i = 0; i < 5; ++i) {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "producing " << i << '\n';
            queue.push(i);
        }   
        queue.close();
    }); 

    std::thread consumer([&]() {
         queue.consume([](int input){
             std::cout << "consuming " << input << '\n';
         });
    }); 

    producer.join();
    consumer.join();
}

1 Ответ

4 голосов
/ 08 апреля 2020

close требует заблокированного владения m при настройке done и notified. И дело не в древней архитектуре, а в современной кэшированной архитектуре. Без заблокированного владения m нет никакой гарантии, что обновленные значения done и notified сброшены за пределы строки кэша close, так что они могут быть прочитаны consume.

Является ли m заблокированным во время cond_var.notify_one() не так важно. И то, и другое верно, и я видел статьи, в которых утверждается, что один или другой являются наиболее производительными.

В целом, любой из них является правильным:

void close()
{
    std::lock_guard lock{m};
    done = true;
    notified = true;
    cond_var.notify_one();
}

или:

void close()
{
    {
        std::lock_guard lock{m};
        done = true;
        notified = true;
    }
    cond_var.notify_one();
}

Обратите внимание, я предполагаю C ++ 17 с отсутствием <std::mutex> на lock_guard. Я мог бы также использовать unique_lock вместо lock_guard здесь, но lock_guard - самый простой инструмент для этой работы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...