Я пытаюсь изучить многопоточность на C++ и не могу понять, почему оба потока не запускаются один за другим? После нажатия мьютекс разблокируется и уведомляется об этом через условную переменную. Почему pop-поток не выполняется в данный момент? Он может получить доступ к очереди, поскольку мьютекс разблокирован, и извлечь вставленное значение. Но вместо этого я вижу, что поток push работает до тех пор, пока очередь не заполнится, а после этого поток pop работает до тех пор, пока очередь не станет пустой.
#include <iostream>
#include <thread>
#include <future>
#include <queue>
using namespace std;
template<typename E>
class BlockingQueue
{
private:
size_t mMaxSize;
queue<E> mQueue;
mutex mtx;
condition_variable mConditionVar;
public:
BlockingQueue(size_t size): mMaxSize{size}
{
}
void push(E element)
{
unique_lock<mutex> pushLock(mtx);
mConditionVar.wait(pushLock, [this](){ return mQueue.size() < mMaxSize; });
cout << "Pushing " << element << endl;
mQueue.push(element);
pushLock.unlock();
mConditionVar.notify_one();
}
void pop()
{
unique_lock<mutex> popLock(mtx);
mConditionVar.wait(popLock, [this](){ return mQueue.size() > 0; });
cout << "Popping " << mQueue.front() << endl;
mQueue.pop();
popLock.unlock();
mConditionVar.notify_one();
}
E front()
{
return mQueue.front();
}
size_t size()
{
return mQueue.size();
}
};
int main()
{
BlockingQueue<int> bq(5);
thread t1([&](){
for (int i = 1; i <= 10; i++)
bq.push(i);
});
thread t2([&](){
for (int i = 1; i<=10; i++)
bq.pop();
});
t1.join();
t2.join();
return 0;
}
@PepijnKramer При вызове notify_one()
есть только один поток ожидания, так что это не будет иметь никакого значения.
@TedLyngmo Да, onlinegdb.com/_M0l4H1Mz. Это дает обоим потокам возможность переоценить свои условия.
@PepijnKramer Результат, который вы получили, мог быть таким же, как и тот, который получил OP. Если я запущу вашу версию в godbolt: godbolt.org/z/55hsbPEd8 — «Это даст обоим потокам возможность переоценить свои условия». — ожидающий поток проснется и проверит условие, а уже работающий поток попытается захватить блокировку. Он не ожидает и на него не повлияет уведомление.
Спасибо, Тед, я это пропустил
@PepijnKramer единственная разница между notify_one
и notify_all
в том, сколько ожидающих потоков будет разблокировано. Поскольку здесь есть только одна потенциальная тема, я не понимаю, как это может изменить ситуацию.
@wohlstad Я знаю это, и я видел более одного работающего потока (нажатие t1 и появление t2) и потенциально ожидающих одной и той же переменной условия... так что я был немного поспешнее. Но теперь я вижу, что условия гарантируют, что любой поток продвигается вперед, так что это нормально.
Когда вызывается mConditionVar.notify_one();
, он пробуждает другой поток. Оба потока теперь запущены и будут конкурировать за блокировку. Иногда поток, который проснулся, получает его, а иногда поток, у которого он был в прошлый раз, получит его снова.
Если вы хотите, чтобы каждый из них запускался один раз, вам нужно изменить условие. Вы можете добавить переменную-член int m_turn = 0;
и использовать ее следующим образом:
void push(E element) {
unique_lock<mutex> pushLock(mtx);
// check if it's my turn:
mConditionVar.wait(pushLock, [this]() { return m_turn == 0; });
cout << "Pushing " << element << endl;
mQueue.push(element);
m_turn = 1; // it's the other thread's turn
mConditionVar.notify_one();
}
void pop() {
unique_lock<mutex> popLock(mtx);
// check if it's my turn:
mConditionVar.wait(popLock, [this]() { return m_turn == 1; });
cout << "Popping " << mQueue.front() << endl;
mQueue.pop();
m_turn = 0; // it's the other thread's turn
mConditionVar.notify_one();
}
Спасибо за идею изменить состояние. Но мой случай, похоже, всегда случается, когда поток, который ранее имел блокировку, всегда получает ее снова. Это не была проблема «иногда», поэтому я подумал, что, возможно, делаю здесь что-то не так. Я думаю, если я заставлю поток спать на какое-то время, он станет более заметным.
@RudranshSrivastava Конечно, сон снизит вероятность повторной блокировки, но это все равно не гарантировано. Для синхронизации обычно следует избегать сна. И да, уже работающий поток может иметь преимущество в том, что он уже запущен и почти всегда получает блокировку.
@RudranshSrivastava, на случай, если вы не знали, вы также можете проголосовать за (голосование и принятие выполняются отдельно). См.: Что мне делать, если кто-то отвечает на мой вопрос?.
@RudranshSrivastava std::mutex
не дает никаких гарантий справедливости, во многом выигрывает тот, кто первым заблокирует блокировку, выход из состояния ожидания, вероятно, занимает больше времени, чем просто продолжение цикла while, поэтому текущий работающий поток имеет тенденцию выигрывать stackoverflow.com/questions/57694091/ …
Из-за поведения потоков и того, как они взаимодействуют с условной переменной и мьютексом.so, чтобы обеспечить более чередующееся поведение, вам необходимо переместить вызов notify_one() внутри заблокированного раздела, прежде чем разблокировать мьютекс. .
Это ничего не решает. Оба потока по-прежнему будут конкурировать за блокировку, когда она будет снята.
В этом случае вам следует использовать:
notify_all()
.