与多个 condition_variable 的双向线程通信具有罕见的挂起/竞争条件

Bidirectional thread communication with multiple condition_variable has rare hang / race condition

我有一个很奇怪的例子,所以我会简单地把上下文放在这里,我们可以假装这是个好主意。

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

主要'thread'(实际上在光纤上但没关系)/游戏循环:

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',但仍会卡住 - 我猜这里存在一个罕见的计时问题。

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

        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();
        }

因为这种情况是由 OS 优化的(它知道 cv 和互斥体之间的 link)。最后,一个消费者意味着:

            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 现在为 false。

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

可能还有其他情况。