C++11 线程与异步性能(VS2013)

Posted

技术标签:

【中文标题】C++11 线程与异步性能(VS2013)【英文标题】:C++11 thread vs async performance (VS2013) 【发布时间】:2014-12-31 01:21:12 【问题描述】:

我觉得我在这里遗漏了一些东西......

我稍微修改了一些代码,从使用std::thread 更改为std::async,并注意到性能有了显着提高。我编写了一个简单的测试,我假设使用std::thread 运行它应该与使用std::async 运行几乎相同。

std::atomic<int> someCount = 0;
const int THREADS = 200;
std::vector<std::thread> threadVec(THREADS);
std::vector<std::future<void>> futureVec(THREADS);
auto lam = [&]()

    for (int i = 0; i < 100; ++i)
        someCount++;
;

for (int i = 0; i < THREADS; ++i)
    threadVec[i] = std::thread(lam);
for (int i = 0; i < THREADS; ++i)
    threadVec[i].join();

for (int i = 0; i < THREADS; ++i)
    futureVec[i] = std::async(std::launch::async, lam);
for (int i = 0; i < THREADS; ++i)
    futureVec[i].get();

我没有深入分析,但一些初步结果表明std::async 代码的运行速度似乎快了 10 倍!关闭优化后结果略有不同,我也尝试切换执行顺序。

这是一些 Visual Studio 编译器问题吗?还是有一些我忽略的更深层次的实现问题会导致这种性能差异?我认为std::asyncstd::thread 调用的包装器?


还考虑到这些差异,我想知道在这里获得最佳性能的方法是什么? (创建线程的不止 std::thread 和 std::async )

如果我想要分离线程怎么办? (据我所知,std::async 无法做到这一点)

【问题讨论】:

如果你有多个 thread::hardware_concurrency() 线程,你就不再使用真正的并发,你的操作系统必须管理上下文切换的开销。顺便说一句,您是否尝试在线程循环中添加 yield() ? 是的,这个例子被夸大了——我这样做是为了看看这两个调用有多“等效”。我仍然注意到一次运行 在 lambda 函数的循环中。目标是简化上下文切换。它不会神奇地消除您的软件线程开销,但它可能会消除一些瓶颈效应。 【参考方案1】:

当您使用异步时,您不会创建新线程,而是重用线程池中可用的线程。创建和销毁线程是一项非常昂贵的操作,在 Windows 操作系统中需要大约 200 000 个 CPU 周期。最重要的是,请记住,线程数远大于 CPU 内核数意味着操作系统需要花费更多时间来创建它们并调度它们以使用每个内核中的可用 CPU 时间。

更新: 为了看到使用std::async 使用的线程数比使用std::thread 小很多,我修改了测试代码以计算运行时使用的唯一线程ID 的数量,如下所示。我的电脑中的结果显示了这个结果:

Number of threads used running std::threads = 200
Number of threads used to run std::async = 4

但在我的 PC 中运行 std::async 的线程数显示从 2 到 4 不等。这基本上意味着std::async 将重用线程而不是每次都创建新线程。奇怪的是,如果我通过在 for 循环中将 100 替换为 1000000 次迭代来增加 lambda 的计算时间,异步线程的数量会增加到 9 但使用原始线程它总是给出 200。值得记住的是 “一旦线程完成,std::thread::id 的值可能会被另一个线程重用”

这里是测试代码:

#include <atomic>
#include <vector>
#include <future>
#include <thread>
#include <unordered_set>
#include <iostream>

int main()

    std::atomic<int> someCount = 0;
    const int THREADS = 200;
    std::vector<std::thread> threadVec(THREADS);
    std::vector<std::future<void>> futureVec(THREADS);

    std::unordered_set<std::thread::id> uniqueThreadIdsAsync;
    std::unordered_set<std::thread::id> uniqueThreadsIdsThreads;
    std::mutex mutex;

    auto lam = [&](bool isAsync)
    
        for (int i = 0; i < 100; ++i)
            someCount++;

        auto threadId = std::this_thread::get_id();
        if (isAsync)
        
            std::lock_guard<std::mutex> lg(mutex);
            uniqueThreadIdsAsync.insert(threadId);
        
        else
        
            std::lock_guard<std::mutex> lg(mutex);
            uniqueThreadsIdsThreads.insert(threadId);
        
    ;

    for (int i = 0; i < THREADS; ++i)
        threadVec[i] = std::thread(lam, false); 

    for (int i = 0; i < THREADS; ++i)
        threadVec[i].join();
    std::cout << "Number of threads used running std::threads = " << uniqueThreadsIdsThreads.size() << std::endl;

    for (int i = 0; i < THREADS; ++i)
        futureVec[i] = std::async(lam, true);
    for (int i = 0; i < THREADS; ++i)
        futureVec[i].get();
    std::cout << "Number of threads used to run std::async = " << uniqueThreadIdsAsync.size() << std::endl;

【讨论】:

@Christophe,我承认内部实现是线程池的证据不多,但至少证明了使用std::async时的线程重用【参考方案2】:

由于您的所有线程都尝试更新相同的atomic&lt;int&gt; someCount,因此性能下降也可能与 contention 相关(确保所有并发访问的原子按顺序排列)。结果可能是:

线程花费时间等待。 但无论如何它们都会消耗 CPU 周期 因此浪费了您的系统吞吐量。

使用async(),在调度中发生一些变化就足够了,这可能会显着减少争用并增加吞吐量。例如,标准规定launch::async 函数对象将被执行“就像在一个由线程对象表示的新执行线程中......”。它并没有说它必须是一个专用线程(所以它可以是——但不一定是——一个线程池)。另一个假设可能是实现采用了更宽松的调度,因为没有说线程需要立即执行(但约束是它在get() 之前执行)。

推荐

基准测试应该在考虑分离关注点的情况下完成。所以为了多线程性能,应该尽量避免线程间同步。

请记住,如果您有超过 thread::hardware_concurrency() 个线程处于活动状态,则不再存在真正的并发性,操作系统必须管理上下文切换的开销。

编辑:一些实验反馈 (2)

对于 100 的 lam 循环,我测量的基准测试结果不可用,因为与 15 毫秒的 Windows 时钟分辨率相关的误差幅度很大。

Test case            Thread      Async 
   10 000 loop          78          31
1 000 000 loop        2743        2670    (the longer the work, the smaler the difference)
   10 000 + yield()    500        1296    (much more context switches) 

当增加THREADS 的数量时,时间会按比例变化,但仅适用于工作时间较短的测试用例。这表明观察到的差异实际上与创建线程时的开销有关,而不是与它们的执行不佳有关。

在第二个实验中,我添加了代码来计算真正涉及的线程数,基于为每次执行存储this_thread::get_id(); 的向量:

对于线程版本,毫不奇怪,总是有 200 个创建(这里)。 非常有趣的是,async() 版本在工作时间较短的情况下会显示 8 到 15 个进程,但当工作时间变长时会显示线程数量增加(在我的测试中最多为 131)。

这表明 async 不是传统的线程池(即具有有限数量的线程),而是重用已经完成工作的线程。这当然会减少开销,尤其是对于较小的任务。 (我相应地更新了我的初始答案)

【讨论】:

我主要投入了原子以防止优化丢弃整个事物,但我将其更改为以宽松的顺序递增,并在两端得到了一些改进的结果 - 所以谢谢! - 但仍然异步击败线程数英里。考虑到时间,线程池的想法听起来是正确的,而且你的产量结果很有趣。 (在使用 windows 进行基准标记时 - 使用 QueryPerformanceCounter,您将获得更好的分辨率) 是的!这也让我感到困惑,我只是用一些额外的观察来编辑答案。 线程池将击败 std::a 同步数英里。线程池中的大多数任务将与主线程中的同步函数一样快地执行,而 std::async 虽然比 std::thread 快,但比普通函数更昂贵。如果要使用线程间同步,最好使用单线程,并将任务作为序列化包启动。

以上是关于C++11 线程与异步性能(VS2013)的主要内容,如果未能解决你的问题,请参考以下文章

C ++如何等待在另一个线程上执行的方法然后主线程完成(VS2010)

2015.3.11 VS异步控件及进度条结合应用

C++11新特性精讲(多线程除外)

C++11新特性精讲(多线程除外)

C++11 VS2013类POD成员初始化

在 vs2013 中禁用 c++11 功能