Двунаправленная связь потока с несколькими condition_variable имеет редкое состояние зависания / гонки

У меня есть довольно странный пример, поэтому я кратко изложу здесь контекст, и мы, надеюсь, сможем просто притвориться, что это хорошая идея.

Я использую профилировщик, который требует регулярных вызовов своего макроса FRAME(), чтобы он знал, где начинаются и заканчиваются кадры ЦП игры (объект, который создает макрос, основан на RAII/области действия). Я использую волокна для потоковой обработки (основной «поток» также является рабочим потоком), и этот макрос профилирования поддерживает вызов только из потока, не зарегистрированного в профилировщике в качестве рабочего потока волокна. Следовательно, у меня есть это ужасное краткосрочное решение, когда я общаюсь с отдельным потоком только для этого макроса. Цель состоит в том, чтобы получить как можно более точное время построения/уничтожения объекта RAII в этом отдельном потоке, не нарушая синхронизацию вызывающего потока. Но иногда зависает все приложение. Я не понимаю, как это возможно здесь.

Основной «поток» (на самом деле на волокне, но это не имеет значения)/игровой цикл:

FrameProfile frameProfile("Client Update");
while (!bShouldQuit)
{
    frameProfile.StartFrame();
    
    /* Do the game client's work for this frame */

    frameProfile.EndFrame();
}

И затем этот объект FrameProfile отвечает за запуск отдельного потока и позволит этому потоку войти в область макроса FRAME, когда StartFrame вызывается из вышеприведенного, и этот поток будет спать в этой области до тех пор, пока не будет вызван EndFrame, после чего он будет проснуться и выйти из области видимости, уничтожив объект измерения кадра профайлера и дав нам, надеюсь, точное время кадра.

struct FrameProfile
{
    FrameProfile(const char* tag)
    {
        pthread_ = std::make_unique<std::thread>(
            [tag, this](std::atomic_bool& killFlag) {
                while (!killFlag)
                {
                    assert(!endThreadFrame.WasSignalled());
                    startThreadFrame.WaitConsume();
                    {
                        assert(!startThreadFrame.WasSignalled());
                        assert(!endedThreadFrame.WasSignalled());

                        // Construct the frame-measuring object using this macro
                        OPTICK_FRAME(tag);

                        startedThreadFrame.Signal();

                        endThreadFrame.WaitConsume();
                        // endThreadFrame has been signalled - we need to exit scope
                        // to finish measuring ASAP
                    }
                    assert(!endThreadFrame.WasSignalled());
                    endedThreadFrame.Signal();
                }
            },
            std::ref(bKill_)
        );
    }

    ~FrameProfile()
    {
        bKill_ = true;
        if (pthread_)
        {
            if (pthread_->joinable())
            {
                pthread_->join();
            }
        }
    }

    void StartFrame()
    {
        assert(!startThreadFrame.WasSignalled());
        assert(!startedThreadFrame.WasSignalled());

        // Tell thread to start measuring the frame
        startThreadFrame.Signal();

        // Wait for thread to have started frame measurement
        startedThreadFrame.WaitConsume();
    }
    void EndFrame()
    {
        assert(!endThreadFrame.WasSignalled());
        assert(!endedThreadFrame.WasSignalled());

        // Tell thread to end frame measurement
        endThreadFrame.Signal();

        // Wait for thread to have ended frame measurement
        endedThreadFrame.WaitConsume();
    }


private:
    std::unique_ptr<std::thread> pthread_;
    std::atomic_bool bKill_ = false;

    struct ThreadSignal
    {
        std::atomic_bool bSignalled;
        std::mutex mutex;
        std::condition_variable cv;

        void Signal()
        {
            assert(!bSignalled);
            {
                std::unique_lock<std::mutex> _(mutex);
                bSignalled = true;
            }
            cv.notify_all();
        }

        bool WasSignalled()
        {
            return bSignalled;
        }

        void WaitConsume()
        {
            std::unique_lock unique(mutex);
            cv.wait(unique, [this]() { return bSignalled == true; });
            unique.unlock();
            bSignalled = false;
        }
    };

    ThreadSignal startThreadFrame;
    ThreadSignal endThreadFrame;

    ThreadSignal startedThreadFrame;
    ThreadSignal endedThreadFrame;
};

Можете ли вы определить, что я делаю неправильно здесь? Или даже гораздо лучшее решение, я был бы открыт для него! Это редко, но иногда зависает — один из объектов «ThreadSignal» будет иметь логическое значение «true», но все равно будет зависать — я думаю, здесь есть редкая проблема с синхронизацией.

Большое спасибо! Рвал на себе волосы.

Пожалуйста, предоставьте минимальный воспроизводимый пример.

Elliott 26.12.2020 01:19

Это не решает вопрос, но когда вы запускаете поток C++ и не отсоединяете его, к нему можно присоединиться. Так что тест в if (pthread_->joinable()) pthread_->join() не нужен.

Pete Becker 26.12.2020 01:33

«Но иногда все приложение зависает. Я не понимаю, как это возможно здесь». -- Когда ваше приложение зависает, вы можете использовать отладчик для проверки состояния вашей программы, когда она зависает. В частности, стек вызовов отдельных потоков может содержать полезную информацию. Например, он должен показывать, ожидает ли поток мьютекс/условную переменную.

Andreas Wenzel 26.12.2020 02:36

@AndreasWenzel действительно, это то, что я сделал, чтобы обнаружить one of the 'ThreadSignal' objects will have its bool as 'true', but will still be stuck. Я просто не знаю, как это технически возможно, если эта двунаправленная сигнализация не позволит потокам рассинхронизироваться - я не думал, что гонка возможна :)

HateDread 26.12.2020 03:05
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
200
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
        std::unique_lock unique(mutex);
        cv.wait(unique, [this]() { return bSignalled == true; });
        unique.unlock();
        bSignalled = false;

это не верно. Переместите назначение в bSignalled внутри блокировки.

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

Изменение этого решит вашу проблему, если я не ошибся.


Также на многих платформах

       assert(!bSignalled);
        {
            std::unique_lock<std::mutex> _(mutex);
            bSignalled = true;
        }
        cv.notify_all();

менее эффективен, чем

       assert(!bSignalled);
        {
            std::unique_lock<std::mutex> _(mutex);
            bSignalled = true;
            cv.notify_all();
        }

так как этот случай оптимизирован ОС (она знает связь между cv и мьютексом). Наконец, один потребитель означает:

            std::unique_lock<std::mutex> _(mutex);
            bSignalled = true;
            cv.notify_one();

верно. Поскольку потребители потребляют сигнал, должен проснуться только один.

   void Signal()
   {
        {
            std::unique_lock<std::mutex> _(mutex);
            bSignalled = true; // 1a
        }
        // 2a
        cv.notify_all(); // 3a
    }

    void WaitConsume()
    {
        std::unique_lock unique(mutex);
        cv.wait(unique, [this]() { return bSignalled == true; }); // 1b
        unique.unlock(); 
        // 2b
        bSignalled = false; //3b
    }

Альфа потока находится на уровне 2a.

Бета-версия темы находится на уровне 2b.

bSignalled имеет значение true, альфа собирается уведомить всех.

Бета-версия темы достигает 3b. bSignalled теперь имеет значение false.

Альфа-поток достигает 3a. Оповещает всех. Любой, кто заметил уведомление, просыпается и видит bSignalled как false. Сообщение потеряно.

Вероятно, есть и другие сценарии.

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