Почему поток push работает до тех пор, пока очередь не достигнет максимального размера? Почему не так - тужь, хлопай, тухай, хлопай?

Я пытаюсь изучить многопоточность на 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;
}

В этом случае вам следует использовать: notify_all().

Pepijn Kramer 10.08.2024 08:16

@PepijnKramer При вызове notify_one() есть только один поток ожидания, так что это не будет иметь никакого значения.

Ted Lyngmo 10.08.2024 08:28

@TedLyngmo Да, onlinegdb.com/_M0l4H1Mz. Это дает обоим потокам возможность переоценить свои условия.

Pepijn Kramer 10.08.2024 08:45

@PepijnKramer Результат, который вы получили, мог быть таким же, как и тот, который получил OP. Если я запущу вашу версию в godbolt: godbolt.org/z/55hsbPEd8 — «Это даст обоим потокам возможность переоценить свои условия». — ожидающий поток проснется и проверит условие, а уже работающий поток попытается захватить блокировку. Он не ожидает и на него не повлияет уведомление.

Ted Lyngmo 10.08.2024 08:54

Спасибо, Тед, я это пропустил

Pepijn Kramer 10.08.2024 09:01

@PepijnKramer единственная разница между notify_one и notify_all в том, сколько ожидающих потоков будет разблокировано. Поскольку здесь есть только одна потенциальная тема, я не понимаю, как это может изменить ситуацию.

wohlstad 10.08.2024 09:02

@wohlstad Я знаю это, и я видел более одного работающего потока (нажатие t1 и появление t2) и потенциально ожидающих одной и той же переменной условия... так что я был немного поспешнее. Но теперь я вижу, что условия гарантируют, что любой поток продвигается вперед, так что это нормально.

Pepijn Kramer 10.08.2024 10:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
7
55
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Когда вызывается 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();
}

демо

Спасибо за идею изменить состояние. Но мой случай, похоже, всегда случается, когда поток, который ранее имел блокировку, всегда получает ее снова. Это не была проблема «иногда», поэтому я подумал, что, возможно, делаю здесь что-то не так. Я думаю, если я заставлю поток спать на какое-то время, он станет более заметным.

Rudransh Srivastava 10.08.2024 08:41

@RudranshSrivastava Конечно, сон снизит вероятность повторной блокировки, но это все равно не гарантировано. Для синхронизации обычно следует избегать сна. И да, уже работающий поток может иметь преимущество в том, что он уже запущен и почти всегда получает блокировку.

Ted Lyngmo 10.08.2024 09:00

@RudranshSrivastava, на случай, если вы не знали, вы также можете проголосовать за (голосование и принятие выполняются отдельно). См.: Что мне делать, если кто-то отвечает на мой вопрос?.

wohlstad 10.08.2024 09:23

@RudranshSrivastava std::mutex не дает никаких гарантий справедливости, во многом выигрывает тот, кто первым заблокирует блокировку, выход из состояния ожидания, вероятно, занимает больше времени, чем просто продолжение цикла while, поэтому текущий работающий поток имеет тенденцию выигрывать stackoverflow.com/questions/57694091/ …

Alan Birtles 10.08.2024 09:44

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

Это ничего не решает. Оба потока по-прежнему будут конкурировать за блокировку, когда она будет снята.

Ted Lyngmo 10.08.2024 09:10

Другие вопросы по теме