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

Ace*_*713 15 c++ multithreading asynchronous c++11 visual-studio-2013

我觉得我在这里错过了一些东西......

我稍微改变了一些代码,从使用到改变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();
Run Code Online (Sandbox Code Playgroud)

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

这是一些Visual Studio编译器问题吗?或者是否有一些更深层次的实施问题我忽略了这会导致这种性能差异?我以为这std::asyncstd::thread电话的包装?


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

如果我想要分离线程怎么样?(就我所知,std :: async不能这样做)

Dar*_*nas 9

当您使用异步时,您不会创建新线程,而是重用线程池中可用的线程.创建和销毁线程是一项非常昂贵的操作,在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
Run Code Online (Sandbox Code Playgroud)

但运行的线程数std::async在我的电脑中显示从2到4的变化.它基本上意味着std::async每次都会重用线程而不是创建新线程.奇怪的是,如果我通过在for循环中用1000000次迭代替换100来增加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;
}
Run Code Online (Sandbox Code Playgroud)

  • 你能提供async正在使用线程池的证据吗? (9认同)

Chr*_*phe 5

由于所有的线程尝试更新相同atomic<int> someCount,性能退化可能也被链接到(原子确保所有concurent访问是sequentialy排序).结果可能是:

  • 线程花时间等待.
  • 但无论如何他们都消耗CPU周期
  • 所以你的系统吞吐量被浪费了.

因此,async()调度的一些变化就足够了,这可能导致争用的显着减少和吞吐量的增加.例如,标准表示launch::async函数对象将"执行" ,就好像在由线程对象表示的新执行线程中...... ".它并没有说它必须是一个专用线程(所以它可以 - 但不一定是 - 一个线程池).另一个假设可能是实现需要更宽松的调度,因为没有任何东西说线程需要立即执行(但是约束是它在之前执行get()).

建议

基准测试应该考虑到关注点的分离.因此,对于多线程性能,应尽可能避免线程间同步.

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

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

如果lam循环为100,由于与Windows时钟分辨率15 ms相关的误差幅度,我测量的基准测试结果无法使用.

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) 
Run Code Online (Sandbox Code Playgroud)

当增加THREADS时间数量的比例逐渐增加时,但仅适用于工作时间短的测试用例.这表明观察到的差异实际上与创建线程开销有关,而不是与执行不良有关.

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

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

这表明async不是传统的线程池(即具有有限数量的线程),而是在线程已经完成工作时重用它们.这当然减少了开销,特别是对于较小的任务. (我相应更新了我的初步答案)