std::thread 的运行速度比 std::future 慢很多

问题描述

我有一些带有 Mainloop 的简单渲染程序,它在一个线程上以大约 8000 fps 的速度运行(除了绘制背景之外什么都不做),我想看看另一个线程渲染是否会在不更改的情况下扰乱当前上下文它(这并不出乎我的意料)。我在这里用这个简单的代码实现了这一点,

m_Thread = std::thread(Mainloop);
m_Thread.join();

这里的代码以某种方式运行得非常慢,大约 30 FPS。我觉得这很奇怪,我记得在另一个项目中我出于类似的基于性能的原因使用了 std::future。所以我然后使用以下代码尝试使用 std::future

m_Future = std::async(std::launch::async,Mainloop);
m_Future.get();

这比单线程性能 (~7900) fps 低一点点。为什么 std::threadstd::future 慢这么多?

编辑:

忽略上面的代码,这里是一个最小的可重现示例,只需将 THREAD 切换为 01 进行比较:

#include <future>
#include <chrono>
#include <Windows.h>
#include <iostream>
#include <string>

#define THREAD 1

static void Function()
{
    
}

int main()
{
    std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::Now();
    std::chrono::high_resolution_clock::time_point finish = std::chrono::high_resolution_clock::Now();
    long double difference = 0;
    long long unsigned int fps = 0;

#if THREAD
    std::thread worker;
#else
    std::future<void> worker;
#endif

    while (true)
    {
        //FPS 
        finish = std::chrono::high_resolution_clock::Now();
        difference = std::chrono::duration_cast<std::chrono::nanoseconds>(finish - start).count();
        difference = difference / 1000000000;
        if (difference > 0.1) {
            start = std::chrono::high_resolution_clock::Now();
            std::wstring fpsstr = L"Fps: ";
            fpsstr += std::to_wstring(fps);
            SetConsoleTitle(fpsstr.c_str());
            fps = 0;
        }
        
#if THREAD
        worker = std::thread(Function);
        worker.join();
#else
        worker = std::async(std::launch::async,Function);
        worker.get();
#endif

        fps += 10;
    }

    return 0;
}

解决方法

std::async 可以通过不同的方式实现。例如,可以有一个预先分配的线程池,每次在循环中使用 std::async 时,您只需重用池中的一个“热”线程。

每次使用 std::thread 时都会创建一个新的系统线程对象。与重用池中的线程相比,这可能是一笔巨大的开销。

我建议您在多线程环境中测试您的代码,在这种环境中,std::async 可能会开始争夺预先分配的系统对象。

,

在某些版本的 MSVC C++ 标准库中,std::async 从(系统)线程池中提取,而 std::thread 不提取。这可能会导致问题,因为我过去已经用尽它并陷入僵局。这也意味着随意使用会更快。

我的建议是在 std::thread 之上编写自己的线程池并使用它。您可以完全控制您有多少活动线程。

这是一个很难解决的问题,但依靠其他人解决它是行不通的,因为老实说,我使用的标准库实现并不能可靠地解决它。

请注意,在 N 大小的线程池中,大小为 N 的阻塞依赖链会死锁。如果您将线程数设为 CPU 数,并且不可靠地重用调用线程,您会发现在 4 核以上机器上测试的多线程代码经常在 2 核机器上死锁。

同时,如果你为每个任务创建一个线程池,并且它们堆叠起来,你最终会导致 CPU 抖动。

请注意,标准对于您实际可以运行的线程数量非常模糊。虽然 std async 必须表现得“好像”你创建了一个新的 std 线程,但实际上这意味着它们必须重新初始化和销毁​​任何 thread_local 对象。

标准中有最终的进度保证,但我在实际实现中看到使用 std::async 时违反了这些保证。所以我现在避免直接使用它。