Состояние гонки 2 чередующихся потока

поэтому я хочу, чтобы программа выводила 1\n2\n1\n2\n1\n2\n, но, кажется, где-то застряла. Но когда я отлаживаю его и устанавливаю точку останова в cv1.notify_one() сразу после объявления t2, он выполняется??

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

using namespace std;

mutex cout_lock;

condition_variable cv1, cv2;
mutex mtx1;
unique_lock<std::mutex> lck1(mtx1);
mutex mtx2;
unique_lock<std::mutex> lck2(mtx2);

const int COUNT = 3;

int main(int argc, char** argv)
{

    thread t1([&](){
        for(int i = 0; i < COUNT; ++i)
        {
            cv1.wait(lck1);
            cout << "1" << endl;
            cv2.notify_one();
        }
    });

    thread t2([&](){
        for(int i = 0; i < COUNT; ++i)
        {
            cv2.wait(lck2);
            cout << "2" << endl;
            cv1.notify_one();
        }
    });

    cv1.notify_one();

    t1.join();
    t2.join();

    return 0;
}

отпустите свои замки, прежде чем уведомлять, может быть?

RmbRT 10.04.2019 00:44

Если вы хотите, чтобы два потока работали синхронно, вам не нужны потоки. Просто напишите две функции и вызывайте их поочередно.

Pete Becker 10.04.2019 22:18
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
183
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я думаю, что у вас есть гонка данных между запуском ваших потоков и вызовом cv1.notify_one(); в main().

Рассмотрим случай, когда вызов cv1.notify_one() происходит до того, как поток 1 запустился и вызвал cv1.wait(). После этого никто больше не звонит cv1.notify и ваши резюме просто ждут. Это называется Потерянное пробуждение.

Вам нужен механизм ожидания в main, пока не запустятся оба потока, а затем выполнить cv1.notify()

Ниже приведен пример использования int и мьютекса.

#include "pch.h"

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

using namespace std;

condition_variable cv1, cv2;
mutex m;

const int COUNT = 3;

enum Turn
{
    T1,
    T2
};

int main(int argc, char** argv)
{
    mutex thread_start_mutex;
    int num_started_threads = 0;
    Turn turn = T1;

    thread t1([&]() {
        {
            // increase the number of started threads
            unique_lock<std::mutex> lck(thread_start_mutex);
            ++num_started_threads;
        }

        for (int i = 0; i < COUNT; ++i)
        {
            // locked cout, unlock before calling notify
            {
                unique_lock<std::mutex> lck1(m);
                // wait till main thread calls notify
                cv1.wait(lck1, [&] { return turn == T1;});
                cout << "1 a really long string" << endl;
                turn = T2; // next it's T2's turn
            }
            cv2.notify_one();
        }
    });

    thread t2([&]() {
        {
            // increase the number of started threads
            unique_lock<std::mutex> lck(thread_start_mutex);
            ++num_started_threads;
        }

        for (int i = 0; i < COUNT; ++i)
        {
            // locked cout, unlock before calling notify
            {
                unique_lock<std::mutex> lck2(m);
                cv2.wait(lck2, [&] {return turn == T2;});
                cout << "2 some other stuff to test" << endl;
                turn = T1;
            }
            cv1.notify_one();
        }
    });

    unique_lock<std::mutex> lck(thread_start_mutex);
    // wait until both threads have started
    cv1.wait(lck, [&] { return num_started_threads == 2; });
    lck.unlock();
    cv1.notify_one();

    t1.join();
    t2.join();

    return 0;
}

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

Редактировать

В моем исходном ответе была точно такая же проблема между вызовами t1.notify() и t2.wait(). Если t1.notify() был вызван до ожидания потока 2, поток 2 никогда не просыпался.

Чтобы решить эту проблему, я добавил перечисление «Turn», которое указывает, чья сейчас очередь, и каждое условие ожидания теперь проверяет, их очередь или нет. Если это так, они не ждут, а просто распечатывают, поэтому, даже если уведомление было пропущено, они все равно выполнят свою задачу. Если это не их очередь, они будут блокироваться до тех пор, пока другой поток не установит переменную поворота и не вызовет уведомление.

ПРИМЕЧАНИЕ: Это демонстрирует хороший пример/практику, что обычно гораздо лучше иметь условие при использовании cv.wait(). Это одновременно проясняет намерения и позволяет избежать как потерянного пробуждения, так и ложного пробуждения.

ЗАМЕТКА 2 это решение может быть слишком сложным, и в целом условные переменные и мьютексы вряд ли будут лучшим решением этой проблемы.

После дальнейших размышлений я думаю, что вы можете избавиться от num_started_threads и использовать только одну condition_variable: 1. Добавление нового значения в Enum с именем «None», которое установлено в начале основного 2. перед cv1.notify() в main, установите значение enum равным T1. 3. замените все вызовы notify_one() на notify_all(). Но это, похоже, немного отличается от вашего исходного решения, поэтому не уверен, что вы хотели бы пойти по этому пути.

David 10.04.2019 03:55

Другой ответ верен концептуально, но все еще имеет другое состояние гонки. Я запустил код, и он все равно заблокировался.

Проблема в том, что t1 создается, но не попадает в cv1.wait(lck1) до тех пор, пока не выполнится cv1.notify_one(). Таким образом, ваши два потока сидят вместе вечно в ожидании. Вы демонстрируете это, когда ставите точку останова на этой строке, позволяя потоку наверстать упущенное. Кроме того, эта проблема сохраняется, когда один поток завершается, но не дает другому времени для вызова wait(), поэтому он просто вызывает notify_one. Это можно увидеть, а также исправить * (используется свободно), добавив несколько вызовов usleep(100) из unistd.h.

Смотри ниже:

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <unistd.h>

using namespace std;

mutex cout_lock;

condition_variable cv1, cv2;
mutex mtx1;
unique_lock<std::mutex> lck1(mtx1);
mutex mtx2;
unique_lock<std::mutex> lck2(mtx2);
const int COUNT = 3;

int main(int argc, char** argv)
{

    thread t1([&](){
        for(int i = 0; i < COUNT; ++i)
        {
            cv1.wait(lck1);
            cout << "1\n";
            usleep(100);
            cv2.notify_one();
        }
    });

    thread t2([&](){
        for(int i = 0; i < COUNT; ++i)
        {

            cv2.wait(lck2);
            cout << "2\n";
            usleep(100);
            cv1.notify_one();
        }
    });

    usleep(1000);
    cv1.notify_one();

    t1.join();
    t2.join();

    return 0;
}

Обновлено: лучше всего было бы проверить ожидающие потоки, которые не встроены в используемые вами мьютексы. Правильным способом может быть создание собственного класса-оболочки мьютекса и включение этой функциональности в класс, но для простоты я просто создал переменную waiting.

Смотри ниже:

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <unistd.h>

using namespace std;

mutex cout_lock;

condition_variable cv1, cv2, cv3;
mutex mtx1;
unique_lock<std::mutex> lck1(mtx1);
mutex mtx2;
unique_lock<std::mutex> lck2(mtx2);
int waiting = 0;
const int COUNT = 3;

int main(int argc, char** argv)
{

    thread t1([&](){
        for(int i = 0; i < COUNT; ++i)
        {
            waiting++;
            cv1.wait(lck1);
            cout << "1\n";
            waiting--;
            if (!waiting)
                usleep(100);
            cv2.notify_one();
        }
    });

    thread t2([&](){
        for(int i = 0; i < COUNT; ++i)
        {
            waiting++;
            cv2.wait(lck2);
            cout << "2\n";
            waiting--;
            if (!waiting)
                usleep(100);
            cv1.notify_one();
        }
    });

    if (!waiting)
        usleep(100);
    cv1.notify_one();

    t1.join();
    t2.join();

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

Есть несколько недостатков:

  1. Вы хотите защитить свой вывод. Поэтому вам нужен только один мьютекс, чтобы только один поток мог выполнять свою работу одновременно.
  2. Вы потенциально пропускаете уведомления для своих переменных состояния.
  3. Ваши глобальные unique_lock захватывают блокировки мьютексов в своих конструкторах. Таким образом, вы все время держите блокировки, и ни один поток не может двигаться вперед. Ваши глобальные unique_lock захватывают блокировки мьютексов в своих конструкторах. Это делается в основном потоке. T1 и T2 разблокируют их через condition_variable. Это поведение undefined (поток, которому принадлежит мьютекс, должен его разблокировать).

Это рецепт правильного использования подхода условной переменной:

  1. Имейте интересующее вас условие. В этом случае какая-то переменная, чтобы помнить, чья сейчас очередь.
  2. Охраняйте эту переменную на (ОДИН!) mutex
  3. Используйте (ОДИН!) condition_variable в сочетании с мьютексом точки 2 и условием точки 1.

Это гарантирует:

  • В любой момент времени есть только один поток, который может посмотреть и/или изменить ваше состояние.
  • Если поток достигает точки в коде, где он, возможно, ожидает условную переменную, он сначала проверяет условие. Возможно, потоку даже не нужно засыпать, поскольку условие, которого он хочет дождаться, уже истинно. Для этого поток должен получить мьютекс, проверить условие и решить, что делать. При этом он владеет замком. Условие не может измениться, потому что поток имеет саму блокировку. Так что вы не можете пропустить уведомление.

Это приводит к следующему коду (посмотреть вживую здесь):

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

using namespace std;

int main(int argc, char** argv)
{
    condition_variable cv;
    mutex mtx;
    bool runt1 = true;
    bool runt2 = false;
    constexpr int COUNT = 3;

    thread t1([&]()
    {
        for(int i = 0; i < COUNT; ++i)
        {
            unique_lock<std::mutex> lck(mtx);
            cv.wait(lck, [&](){ return runt1; });
            cout << "1" << endl;
            runt1 = false;
            runt2 = true;
            lck.unlock();
            cv.notify_one();
        }
    });

    thread t2([&]()
    {
        for(int i = 0; i < COUNT; ++i)
        {
            unique_lock<std::mutex> lck(mtx);
            cv.wait(lck, [&](){ return runt2; });
            cout << "2" << endl;
            runt1 = true;
            runt2 = false;   
            lck.unlock();
            cv.notify_one();
        }
    });

    t1.join();
    t2.join();

    return 0;
}

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