C ++多线程运行时问题

时间:2016-05-17 03:57:32

标签: c++ multithreading

我一直在研究C ++多线程并得到一个问题。

以下是我对多线程的理解。 我们使用多线程的原因之一是缩短运行时间,对吧? 例如,我认为如果我们使用两个线程,我们可以期待一半的执行时间。 所以,我尝试编码来证明它。 这是代码。

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

using namespace std;
#define iterationNumber 1000000

void myFunction(const int index, const int numberInThread, promise<unsigned long>&& p, const vector<int>& numberList) { 
    clock_t begin,end;
    int firstIndex = index * numberInThread;
    int lastIndex = firstIndex + numberInThread;
    vector<int>::const_iterator first = numberList.cbegin() + firstIndex;
    vector<int>::const_iterator last = numberList.cbegin() + lastIndex;

    vector<int> numbers(first,last);

    unsigned long result = 0;

    begin = clock();
    for(int i = 0 ; i < numbers.size(); i++) {
        result += numbers.at(i);
    }
    end = clock();
    cout << "thread" << index << " took " << ((float)(end-begin))/CLOCKS_PER_SEC << endl;

    p.set_value(result);

}


int main(void)
{
    vector<int> numberList;
    vector<thread> t;
    vector<future<unsigned long>> futures;
    vector<unsigned long> result;
    const int NumberOfThreads = thread::hardware_concurrency() ?: 2;
    int numberInThread = iterationNumber / NumberOfThreads;

    clock_t begin,end;


    for(int i = 0 ; i < iterationNumber ; i++) {
        int randomN =  rand() % 10000 + 1;
        numberList.push_back(randomN);
    }

    for(int j = 0 ; j < NumberOfThreads; j++){
        promise<unsigned long> promises;
        futures.push_back(promises.get_future());
        t.push_back(thread(myFunction, j, numberInThread, std::move(promises), numberList));
    }

    for_each(t.begin(), t.end(), std::mem_fn(&std::thread::join));

    for (int i = 0; i < futures.size(); i++) {
        result.push_back(futures.at(i).get());
    }

    unsigned long RRR = 0;

    begin = clock();
    for(int i = 0 ; i < numberList.size(); i++) {
        RRR += numberList.at(i);
    }
    end = clock();
    cout << "not by thread took " << ((float)(end-begin))/CLOCKS_PER_SEC << endl;

}

因为我的笔记本电脑的硬件并发性是4,它将创建4个线程,每个线程需要四分之一的数字列表并总结数字。

然而,结果与我的预期不同。

thread0 took 0.007232
thread1 took 0.007402
thread2 took 0.010035
thread3 took 0.011759
not by thread took 0.009654

为什么呢?为什么花了比串行版本更多的时间(而不是线程)。

4 个答案:

答案 0 :(得分:3)

  

例如,我认为如果我们使用两个线程,我们可以期待一半   执行时间。

你是这么认为的,但遗憾的是,实际情况往往并非如此。理想的“N核意味着执行时间的1 / N”场景仅在N个核心可以完全并行执行时发生,而没有任何核心的动作干扰其他核心的性能。

但是你的线程正在做的只是总结一个数组的不同子部分......当然可以从并行执行中获益吗?答案是,原则上它可以,但在现代CPU上,简单的添加是如此快速,以至于它不是一个完整循环需要多长时间的因素。真正 限制循环执行速度的是访问RAM。与CPU的速度相比,RAM访问速度非常慢 - 在大多数台式计算机上,每个CPU只有一个与RAM的连接,无论它有多少核心。这意味着你在程序中真正测量的是从RAM到CPU读取大量整数的速度,并且速度大致相同 - 等于CPU的内存总线带宽 - 无论它是一个核心读取内存,还是四个。

为了演示RAM访问量是多少因素,下面是测试程序的修改/简化版本。在这个版本的程序中,我删除了大向量,而计算只是对(相对昂贵的)sin()函数的一系列调用。请注意,在此版本中,循环只访问几个内存位置,而不是数千个,因此运行计算循环的核心不必定期等待更多数据从RAM复制到其本地缓存:

#include <vector>
#include <iostream>
#include <thread>
#include <chrono>
#include <math.h>

using namespace std;

static int iterationNumber = 1000000;

unsigned long long threadElapsedTimeMicros[10];
unsigned long threadResults[10];

void myFunction(const int index, const int numberInThread)
{
   unsigned long result = 666;

   std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
   for(int i=0; i<numberInThread; i++) result += 100*sin(result);
   std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();

   threadResults[index] = result;
   threadElapsedTimeMicros[index] = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();

   // We'll print out the value of threadElapsedTimeMicros[index] later on,
   // after all the threads have been join()'d.
   // If we printed it out now it might affect the timing of the other threads
   // that may still be executing
}

int main(void)
{
    vector<thread> t;
    const int NumberOfThreads = thread::hardware_concurrency();
    const int numberInThread  = iterationNumber / NumberOfThreads;

    // Multithreaded approach
    std::chrono::steady_clock::time_point allBegin = std::chrono::steady_clock::now();
    for(int j = 0 ; j < NumberOfThreads; j++) t.push_back(thread(myFunction, j, numberInThread));
    for(int j = 0 ; j < NumberOfThreads; j++) t[j].join();
    std::chrono::steady_clock::time_point allEnd = std::chrono::steady_clock::now();

    for(int j = 0 ; j < NumberOfThreads; j++) cout << " The computations in thread #" << j << ": result=" << threadResults[j] << ", took " << threadElapsedTimeMicros[j] << " microseconds" << std::endl;
    cout << " Total time spent doing multithreaded computations was " << std::chrono::duration_cast<std::chrono::microseconds>(allEnd - allBegin).count() << " microseconds in total" << std::endl;

    // And now, the single-threaded approach, for comparison
    unsigned long result = 666;
    std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
    for(int i = 0 ; i < iterationNumber; i++) result += 100*sin(result);
    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();

    cout << "result=" << result << ", single-threaded computation took " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << " microseconds" << std::endl;
    return 0;
}

当我在我的双核Mac mini(带有超线程的i7)上运行上述程序时,我得到的结果如下:

Jeremys-Mac-mini:~ lcsuser1$ g++ -std=c++11 -O3 ./temp.cpp
Jeremys-Mac-mini:~ lcsuser1$ ./a.out
 The computations in thread #0: result=1062, took 11718 microseconds
 The computations in thread #1: result=1062, took 11481 microseconds
 The computations in thread #2: result=1062, took 11525 microseconds
 The computations in thread #3: result=1062, took 11230 microseconds
 Total time spent doing multithreaded computations was 16492 microseconds in total
result=1181, single-threaded computation took 49846 microseconds

因此,在这种情况下,结果更像您期望的结果 - 因为内存访问不是瓶颈,每个核心都能够全速运行,并在大约25个时间内完成其总计算的25%部分单个线程完成100%计算所花费的时间的百分比...并且由于四个核心并行运行,因此计算所花费的总时间约为单个时间的33%。 - 线程例程完成(理想情况下它将是25%,但启动和关闭线程会涉及一些开销等)。

答案 1 :(得分:2)

这是初学者的解释。

它在技术上并不准确,但恕我直言,任何人都不会因为阅读而受到损害。

它提供了理解并行处理术语的入口。

主题,任务和流程

了解线程和进程之间的区别非常重要。 默认情况下,启动新进程,为该进程分配专用内存。因此,他们共享内存而没有其他进程,并且(理论上)可以在不同的计算机上运行。 (您可以通过其他进程共享内存,通过操作系统或共享内存&#34;,但您必须添加这些功能,默认情况下它们不适用于您的进程)

拥有多个核心意味着每个正在运行的进程都可以在任何空闲核心上执行。 所以基本上一个程序在一个核心上运行,另一个程序在第二个核心上运行,后台服务为你做一些事情,运行在第三个核心上,(依此类推等等)

线程是不同的东西。 例如,所有进程都将在主线程中运行。 操作系统实现了一个调度程序,它应该为程序分配CPU时间。原则上它会说:

  • 程序A,得到0.01秒,而不是暂停!
  • 程序B,获得0.01秒,然后暂停!
  • 程序A,获得0.01秒,然后暂停!
  • 程序B,获得0.01秒,然后暂停!

你明白了......

调度程序通常可以在线程之间进行优先级排序,因此某些程序比其他程序获得更多的CPU时间。

调度程序当然可以在所有核心上调度线程,但是如果它在一个进程中执行此操作,(在多个核心上拆分进程的线程),则每个核心都会保留它的性能损失。拥有非常快的内存缓存。 由于来自同一进程的线程可以访问相同的缓存,因此在线程之间共享内存非常快。

访问另一个核心缓存的速度并不快(如果不通过RAM也可以),因此通常调度程序不会在多个核心上拆分进程。 结果是属于进程的所有线程都在同一个核心上运行。

| Core 1               | Core 2              | Core 3             |
| Process A, Thread 1  | Process C, Thread 1 | Process F, Thread 1|
| Process A, Thread 2  | Process D, Thread 1 | Process F, Thread 2|
| Process B, Thread 1  | Process E, Thread 1 | Process F, Thread 3|
| Process A, Thread 1  | Process C, Thread 1 | Process F, Thread 1|
| Process A, Thread 2  | Process D, Thread 1 | Process F, Thread 2|    
| Process B, Thread 1  | Process E, Thread 1 | Process F, Thread 3|

一个进程可以生成多个线程,它们都共享父线程内存区域,并且通常都运行在父运行的核心上。

如果您的应用程序需要响应无法控制时间的内容,那么在进程中生成线程是有意义的。 I.E.当应用程序运行需要很长时间才能完成的计算时,用户按下取消按钮或尝试移动窗口。

UI的响应性要求应用程序花时间阅读,并处理用户尝试执行的操作。如果程序在每次迭代中执行部分计算,则可以在主循环中实现。 然而,这变得非常复杂,因此不是使用计算代码,而是在计算过程中退出以检查UI,并更新UI,然后继续。您在另一个线程中运行计算代码。 然后,调度程序确保UI线程和计算线程获得CPU时间,因此UI响应用户输入,同时计算继续。 而且你的代码保持相当简单。

但我希望运行我的计算另一个核心来提高速度

要在多个核心上分配计算,您可以为每个计算作业生成一个新进程。通过这种方式,调度程序将知道每个进程都拥有自己的内存,并且可以很容易地在空闲内核上启动。

但是您遇到问题,需要与其他进程共享内存,因此它知道该怎么做。 这样做的一种简单方法是通过文件系统共享内存。 您可以使用计算数据创建一个文件,然后生成一个管理执行(和通信)的线程与另一个程序,(因此您的UI响应,我们等待结果)。 管理线程通过系统命令运行其他程序,系统命令将其作为另一个进程启动。 另一个程序将被编写为使用输入文件作为输入参数运行,因此我们可以在不同文件的多个实例中运行它。 如果程序在完成时自行终止并创建输出文件,它可以在任何核心(或多个)上运行,并且您的进程可以读取输出文件。

这实际上是有效的,如果计算需要很长时间(比如很多分钟),这可能没问题,即使我们使用文件在我们的进程之间进行通信。

但是,对于只需几秒钟的计算,文件系统很慢,等待它几乎会删除使用进程而不仅仅是使用线程所获得的性能。因此,在现实生活中使用其他更有效的内存共享。例如,在RAM中创建共享内存区域。

创建管理线程,并生成子流程,允许通过管理线程与进程通信,在进程完成时收集数据,并通过管理线程公开&#34;可以通过多种方式实施。

<强>任务 那么&#34;任务&#34;很暧昧。

一般来说,它意味着&#34;解决任务的过程或线程&#34;。

然而,在某些语言(如C#)中,它实现了线程,如事物,调度程序可以将其视为一个进程。其他提供类似功能的语言通常称为任务或工作人员。

因此,对于worker / tasks而言,程序员似乎只是一个线程,通过调用线程上的方法,您可以轻松地通过引用和控制共享内存,就像任何其他线程一样。

但调度程序似乎是一个可以在任何核心上运行的进程。 它以一种相当有效的方式实现共享内存问题,作为语言的一部分,因此程序员不必为所有任务重新发明这个轮子。

这通常被称为&#34;混合线程&#34;或者只是&#34;并行线程&#34;

答案 2 :(得分:0)

似乎你对多线程有一些误解。简单地使用两个线程不能将处理时间减半。

多线程是一种复杂的概念,但您可以在网上轻松找到相关资料。你应该先读一个。但我会尝试用一个例子给出一个简单的解释。

无论您拥有多少CPU(或核心),无论您使用多线程还是不使用,CPU的总处理能力始终相同,对吧?那么,性能差异来自哪里?

当程序在设备(计算机)上运行时,它不仅使用CPU,还使用其他系统资源,如网络,RAM,硬盘驱动器等。如果程序流程序列化,则会有一定的时间点当CPU空闲等待其他系统资源完成时。但是,如果程序以多个线程(多个流程)运行,如果线程变为空闲(等待其他系统资源完成某些任务),则其他线程可以使用CPU。因此,您可以最小化CPU的空闲时间并提高时间性能。这是关于多线程的最简单的例子之一。

由于您的示例代码几乎“只消耗CPU”,因此使用多线程可能会带来很少的性能提升。 有时可能会更糟,因为多线程也会带来上下文切换的时间成本。

仅供参考,parallel processing与多线程不同。

答案 3 :(得分:0)

非常好地指出macs的问题。

如果你使用o.s.可以以有用的方式调度线程,你必须考虑问题是否基本上是1个问题的产物多次。一个例子是矩阵乘法。当你乘以2个矩阵时,它的某些部分与其他部分无关。 3x3矩阵乘以另外3x3需要9个点积,可以独立于其他积分计算,它们本身需要3次乘法和2次加法,但这里必须先进行乘法运算。所以我们看看我们是否想要利用多线程处理器完成这项任务,我们可以使用9个内核或线程,并且假设它们具有相同的计算时间或具有相同的优先级(可在窗口上调整),则可以减少乘以3x3矩阵的时间。 9.这是因为我们基本上做了9次,可以由9个人同时完成。

现在对于9个线程中的每个线程,我们可以有3个内核执行乘法,总共3x9 = 24个核心现在一起。减少时间t / 24。但是我们有18个新增功能,在这里我们无法从更多内核中获得收益。一个添加必须通过管道传输到另一个。问题需要时间t,一个核心或时间t / 24理想情况下24个核心协同工作。现在你可以看到为什么问题经常被找到,如果他们是线性的&#39;因为它们可以并行完成,例如图形(有些像背面剔除是排序问题而且本质上不是线性的,因此并行处理会降低性能提升)。

然后增加了启动线程的开销以及o.s.如何安排线程。和处理器。希望这可以帮助。