【问题标题】:std::thread runs A LOT slower than std::futurestd::thread 运行速度比 std::future 慢很多
【发布时间】:2021-07-11 23:46:51
【问题描述】:

我有一个带有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;
}

【问题讨论】:

  • 这里的差异似乎是统计噪音和微不足道的。也许慢了一点,但远不及“这么慢”。
  • 你的编译器和版本是什么?对于 Windows,可能有 async 的后台线程池
  • @SamVarshavchik ??不将未来与单线程进行比较,将 std::thread 与 std::future 进行比较,30 fps -> 7900 fps 非常重要?
  • 取决于实现。我有一个应用程序使用异步运行速度比单线程快 5 倍,使用 MSVC 线程运行速度快 3 倍。那是在6核系统上。创建线程有开销,而异步使用开销较少的线程池。但是您还需要在线程中做足够的工作来克服开销。并且您需要优化内存使用,以便线程不会访问彼此的内存。
  • 创建thread 并加入它就像告诉某人购买一辆新车并将其开到商店然后出售汽车;而async 就像告诉某人打车去商店一样。您可以希望看到为什么线程较慢。出租车公司管理着一支活跃的车队,他们当然不会每次有人叫出租车时都买一辆新车。

标签: c++ multithreading c++11 future stdthread


【解决方案1】:

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

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

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

【讨论】:

  • "std::async 可以用不同的方式实现" 当你使用启动策略async 时不会。该启动策略本身必须意味着该函数被调用“就像在由thread 对象表示的新执行线程中一样”。而那个“好像”部分意味着thread_local 必须正确初始化。如果这没有发生,那么它就不是 async 启动策略的正确实施。
  • @NicolBolas 我正在尝试查看您的评论的相关性。您能否进一步解释一下您认为这对给出的答案有何影响?
  • “好像”并不意味着必须创建一个新线程。理论上,每个std::async 可能会使用一个额外的线程:在这种情况下,一次只会运行一个异步过程,其余的将等待它完成。
  • @NicolBolas,另外我建议你研究一下 executor 在 C++23 中会做什么。执行器允许您更好地控制复杂对象(如期货)的执行方式;如果没有执行者,可能会有很多不同的可能实现不违反标准。
  • @Galik:“您能否进一步解释一下您认为这对给出的答案有何影响?”这意味着,对于async 启动策略,实现是不允许重用现有线程。它必须创建“一个新线程”。如果这没有发生,那么实施是不正确的。请注意,deferred|async 策略不需要创建“新线程”,因此线程池是有效的实现。
【解决方案2】:

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

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

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

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

同时,如果您为每个任务创建一个线程池,然后将它们堆叠起来,最终会导致 CPU 崩溃。

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

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

【讨论】:

    猜你喜欢
    • 2020-12-15
    • 2015-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-13
    • 2012-02-05
    相关资源
    最近更新 更多