【问题标题】:Bidirectional thread communication with multiple condition_variable has rare hang / race condition具有多个条件变量的双向线程通信具有罕见的挂起/竞争条件
【发布时间】:2020-12-26 00:12:12
【问题描述】:

我有一个相当奇怪的例子,所以我将在这里简要介绍一下上下文,希望我们可以假装这是一个好主意。

我正在使用需要定期调用其 FRAME() 宏的分析器,以便它知道游戏的 CPU 帧在哪里开始和结束(宏构建的对象是基于 RAII/范围的)。我正在为我的线程使用光纤(主“线程”也是一个光纤工作线程),并且这个分析宏只支持从一个未在分析器中注册为光纤工作线程的线程调用。因此,我在短期内有这个糟糕的解决方案,我只为这个宏与一个单独的线程通信。目标是在此单独线程上尽可能准确地获取 RAII 对象的构造/销毁时间,而不会中断调用线程的时间。但有时,整个应用程序都会挂起。我不明白这怎么可能。

主“线程”(实际上是在光纤上,但没关系)/游戏循环:

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

    frameProfile.EndFrame();
}

然后这个 FrameProfile 对象负责启动一个单独的线程,并在从上面调用 StartFrame 时让该线程进入 FRAME 宏范围,并且该线程将在该范围内休眠,直到调用 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',但仍会卡住 - 我想这里有一个罕见的时间问题。

非常感谢!一直在扯我的头发。

【问题讨论】:

  • 这并没有解决问题,但是当你启动一个 C++ 线程并且你不分离它时,它是可连接的。所以不需要if (pthread_-&gt;joinable()) pthread_-&gt;join() 中的测试。
  • “但有时,整个应用程序都会挂起。我不明白这怎么可能。” -- 当您的应用程序挂起时,您可能希望使用调试器来检查程序挂起时的状态。特别是各个线程的调用堆栈可能包含有用的信息。例如,它应该显示线程是否正在等待互斥体/条件变量。
  • @AndreasWenzel 确实,这就是我发现one of the 'ThreadSignal' objects will have its bool as 'true', but will still be stuck 所做的。我只是不知道如果这种双向信号不会让线程不同步,这在技术上是如何实现的——我认为不可能进行比赛:)

标签: c++ multithreading communication race-condition


【解决方案1】:
        std::unique_lock unique(mutex);
        cv.wait(unique, [this]() { return bSignalled == true; });
        unique.unlock();
        bSignalled = false;

这是错误的。将分配移动到 bSignalled inside lock。

基本上,在条件互斥体之外交出条件状态。有一些狭隘的方法可以证明它是合法的,但在你这样做之前,先写一个证明并记录下来,因为我见过的每一种合法的方法都非常脆弱;下一个接触您的代码的人可以轻松破解它。

改变它会解决你的问题,除非我弄错了。


也在许多平台上

       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
    }

线程 alpha 位于 2a。

线程测试版在 2b。

bSignalled 为真,alpha 即将通知所有人。

线程测试版达到 3b。 bSignalled 现在为假。

线程 alpha 达到 3a。它通知所有人。任何注意到通知的人都会醒来并看到 bSignalled 为假。消息丢失。

可能还有其他情况。

【讨论】:

    猜你喜欢
    • 2012-11-17
    • 1970-01-01
    • 1970-01-01
    • 2023-01-30
    • 2018-09-10
    • 1970-01-01
    • 1970-01-01
    • 2016-10-12
    • 1970-01-01
    相关资源
    最近更新 更多