условие, переменная, ссылки и темы: кому принадлежит The Lock? - PullRequest
1 голос
/ 10 февраля 2020

Предположим, у меня есть класс ThreadQueue, содержащий std::queue, и я передаю его экземпляр в std::ref потоку. Предположим далее, что этот поток 1 (основной поток) создает и хранит объект ThreadQueue и помещает в него сообщения, а задача второго потока состоит в том, чтобы принимать эти сообщения по мере их поступления и помещать их где-нибудь, скажем, записывать их в файл журнала.

Класс выглядит следующим образом:

#include <queue>
#include <mutex>
#include <condition_variable>

using namespace std;

template <typename T>
class ThreadQueue
{
    queue<T> q_;
    mutex mtx;
    unique_lock<mutex> lck;
    condition_variable cv;

public:
    ThreadQueue() { lck = unique_lock<mutex>(mtx); }
    ~ThreadQueue() { if (lck.owns_lock()) lck.unlock(); }

    void enqueue (const T&);
    T dequeue ();
};

template <typename T>
void ThreadQueue<T>::enqueue (const T& t)
{
    lck.lock();
    q_.push(t);
    lck.unlock();
    cv.notify_one();
}

template <typename T>
T ThreadQueue<T>::dequeue ()
{
    cv.wait(lck);
    lck.lock();
    T t = q_.front(); // let's assume that's a copy assignment, because
    q_.pop();         // pop() calls the descructor.
    lck.unlock();
    return t;
}

Затем в основном звучит мелодия:

ThreadQueue<std::pair<int, std::string>> logs;
// and maybe something like:
std::thread logger(std::ref(logs));

Важнейшая строка - cv.wait(lck); В документации четко указано, что lck требуется объект unique_lock, объект мьютекса которого в данный момент заблокирован этим потоком.

Теперь возникает вопрос: кто на самом деле блокирует мьютекс и кому принадлежит блокировка, поток 1 или нить 2?

1 Ответ

5 голосов
/ 10 февраля 2020

В коде есть две основные ошибки:

  1. unique_lock не должно быть переменной-членом. Он должен быть создан в стеке, чтобы при выходе из области действия (либо при нормальном возврате, либо при исключении) блокировка автоматически снималась для вас.
  2. cv.wait необходимо вызывать только после того, как вы проверили, что очередь действительно пуста. std::condition_variable - это механизм связи без сохранения состояния, если при отсутствии сигнала официантов сигнал теряется. Есть также ложные пробуждения. Вы можете использовать cv.wait([this] { return !q_.empty(); });, который правильно обрабатывает ожидание переменной условия.

Например:

using namespace std;

template <typename T>
class ThreadQueue
{
    queue<T> q_;
    mutex mtx;
    condition_variable cv;

public:
    void enqueue (const T&);
    T dequeue ();
};

template <typename T>
void ThreadQueue<T>::enqueue (const T& t)
{
    {
        lock_guard<mutex> lck(mtx);
        q_.push(t);
    }
    cv.notify_one(); // Optimization: release the lock before signalling.
}

template <typename T>
T ThreadQueue<T>::dequeue ()
{
    unique_lock<mutex> lck(mtx);
    cv.wait(lck, [this] { return !q_.empty(); });
    T t = q_.front();
    q_.pop();
    return t;
}

Кому принадлежит блокировка?

Поток, который заблокировал мьютекс, владеет блокировкой или вошел в критическую секцию . И std::lock_guard, и std::unique_lock здесь блокируют мьютекс в конструкторе и разблокируют в деструкторе (при нормальном выходе из области видимости или исключении).

...