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

16

我感觉自己在这里缺少了什么...

我稍微改了一些代码,从使用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()? - Christophe
是的,这个例子有些夸张 - 我这样做是为了看看这两个调用有多“等价”。我仍然注意到在同时运行不到10个线程时有所不同。而且,我没有放置任何yield()...你建议我在哪里添加它?它在这里可能会起到什么作用? - Ace24713
在您的 Lambda 函数循环中。目标是为了简化上下文切换。它不会神奇地消除您的软件线程开销,但它可能会平滑一些瓶颈效应。 - Christophe
2个回答

11
当您使用async时,您不会创建新线程,而是重复使用线程池中可用的线程。在Windows操作系统中,创建和销毁线程是非常昂贵的操作,需要大约200,000个CPU周期。此外,请记住,拥有比CPU核心数量更多的线程意味着操作系统需要花费更多时间来创建它们并安排它们在每个核心中使用可用的CPU时间。
更新: 为了查看使用std::async使用的线程数要比使用std::thread小得多,我已修改测试代码以计算在任何一种方式下运行时使用的唯一线程ID数。我的PC显示以下结果:
Number of threads used running std::threads = 200
Number of threads used to run std::async = 4

但是在我的电脑上,运行 std::async 的线程数量会从2到4不等。这基本意味着 std::async 会重复使用现有的线程,而不是每次都创建新的线程。有趣的是,如果我通过将循环中的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时线程的重用。 - Darien Pardinas

5
由于所有线程都尝试更新相同的atomic someCount,性能下降可能与争用(原子保证所有并发访问按顺序排序)有关。其结果可能是:
1. 线程花费时间等待。 2. 但它们仍然消耗CPU周期。 3. 因此,系统吞吐量被浪费。
使用async()函数,只需要一些调度变化即可显著减少争用并提高吞吐量。例如,标准指出,launch::async函数对象将“如同由表示为线程对象的新执行线程…”执行。它没有说必须是专用线程(因此可以是线程池,也可以不是)。另一个假设是实现采用更轻松的调度,因为没有任何东西表明线程需要立即执行(但约束是在get()之前执行)。
建议考虑实现分离关注点进行基准测试。因此,在多线程性能方面,应尽可能避免线程间同步。
请记住,如果您有超过thread::hardware_concurrency()个活动线程,则不再有真正的并发,并且操作系统必须管理上下文切换的开销。
编辑:一些实验反馈(2)
使用100次lam循环,我测量的基准结果不可用,因为与Windows时钟分辨率15毫秒相关的误差量太大。
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不使用传统的线程池(即具有有限线程数的线程池),而是在已完成工作的情况下重复使用线程。这当然减少了开销,特别是对于较小的任务。(我根据这个更新了我的初始答案)

1
我主要加入了 atomic 来防止优化丢弃整个内容,但我将其更改为使用 relaxed order 进行增量,并在两端获得了一些改进的结果 - 所以感谢您!- 但仍然是 async 比 thread 更好。根据时间安排,线程池的想法听起来是正确的,而您的 yield 结果也很有趣。(在 Windows 上进行基准测试时,请使用 QueryPerformanceCounter,您将获得更好的分辨率) - Ace24713
是的!这也困扰了我,我刚刚编辑了答案并附加了一些观察。 - Christophe
线程池比std::async快得多。线程池中的大多数任务将像主线程中的同步函数一样快速执行,而std::async虽然比std::thread更快,但比普通函数更昂贵。如果要使用线程间同步,则最好使用单个线程,并将任务作为序列化包启动。 - ark1974

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接