多核多线程技术编程

来源:互联网 发布:java反编译怎么用 编辑:程序博客网 时间:2024/04/28 07:31

几年之前,CPU的性能还主要取决于CPU的主频,经过超摩尔定律的发展后,没过多长时间CPU的主频速度就已接近“极限”,使得单单靠提高CPU的主频来提升性能变得非常困难。
    目前,Intel、AMD等CPU生产商都转而采用了多核技术来提升CPU性能,甚至提出了群核CPU的概念。这意味着,要充分发挥多核CPU的性能,程序就必须采用多线程并发计算的方式,传统的串行程序将会极大地浪费多核CPU的运算能力!

C++是上世纪80年代诞生的语言,它的前身是同样风靡全球的C语言。一直以来它都以代码效率卓越著称,进入多核时代后,因为C++标准库没有提供多线程支持,要用C++开发出充分利用多核CPU的程序将面临很大挑战。
    于是,在C++社区出现了不少优秀的库以支持并行编程,如各种跨平台的线程库,OpenMP,Clik++等。另一方面,微软也从Win2K开始不断地加入线程池API(如QueueUserWorkItem),C++09标准也明确地表示要加入多线程的支持。

使用线程库编写并行程序的优点是可以精确调度各个线程,并且可以在所有C++编译器里使用。不过要充分发挥多核CPU的性能,还要考虑很多因素,主要难点有:

·         死锁    编写多线程必然会遇到同步问题,如果同步控制出现问题,就可能出现死锁或脏数据。

·         线程之间通信    使用何种机制在多个线程之间通信?即要保证通信数据同步又要保证效率。

·         负载平衡    分配到每个线程的工作量要尽量平衡,避免一个线程忙一个线程闲的情形发生。

·         资源匹配    程序应该使用多少个线程?过少的线程不能充分利用CPU的多核优势,而过多的线程会造成线程调度过于频繁同样会降低效率。

 OpenMP是目前比较流行的C++并行编程方式,它通过在代码中插入专用的pragma编译指令来指示编译器把串行代码编译成并行程序。
    它的优点是易于使用,几乎不用修改原代码就可对老程序进行并发支持的改造。问题是它必须要有编译器的支持,尽管目前不少编译器都提供了OpenMP的支 持,但它毕竟不是C++的一部分,甚至它都不是真正意义上的C++库。。

现在,我们又有了一个新选择:Intel Thread Building Blocks(TBB,线程构建模块)。TBB是一个开源的C++模板库,能够运行在 Windows、Linux、Macintosh以及UNIX等系统上,只要是标准的C++编译器都可以使用它。

以下是程序测试的实验案例和测试结果:

for ( int j = 0; j < num; j++ )
    {  
        k[j]=j;
         for(int p=0;p<10000;p++)cout(p); //
进行很大的for循环,又调用函数,主要目的就是让它耗时间;
    }    
    for(int i=0;i<num;i++)     //
顺序输出0-99;
        printf("%d ",k[i]);

    很显然,这是一个经典的单线程程序,其中的for(int p=0;p<10000;p++)cout(p); 是一个内嵌了提高程序复杂性的函数的大循环,主要是为了提高程序的额外开销,以便于我们能够明显的观察到多线程程序与单线程程序之间的性能差异。整个for程序执行的流程如下所示:

     j=0 -> k[j]=j -> 进行复杂运算 -> j!=j++并继续下一个循环num-1?:退出循环并顺序显示k[num]中的内容

    有一点需要注意的是,由于单线程的程序是顺序执行的,所以上面这个程序第二个for语句其实是可以不必使用的,可以将它内嵌入第一个for语句中:

     for ( int j = 0; j < num; j++ )
    {  
        k[j]=j;
         for(int p=0;p<10000;p++)cout(p); //进行很大的for循环,又调用函数,主要目的就是让它耗时间;

        printf("%d ",k[i]);
    }

这样可以减少程序额外开销,但是对于多线程程序来说,这点开销是必须的,如果输出必须是顺序的话,那我们有必要控制它的输出顺序,否则将会出现乱序输出--尽管结果是正确的,但是输出的顺序却是我们不想看到的。当然,第二个for语句所增加的开销,远远比不上并行程序运行时所节约的开销。我们使用Intel的openmp技术来创建多线程的程序,因为openmp技术够直观,也很容易去分析与理解,所以我们无需去调用底层API就能够轻易的实现多线程编程。要使用openmp技术就要安装Intel编译器及下面的openmp组件,Intel编译器可以很好的与Visual Stdio整合在一起,起码要求是你的计算机上必须安装了Visual C++6.0。当然,安装完之后我们必须进行一些设置,以Visual Stdio 2008为例,如果我们想要使用openmp技术的话,我们须在VC项目下右边的“资源管理器”中点“属性”--“C/C++”--“语言”--有个“openmp支持”选项,选“是(/openmp)”即可。另外我们必须在程序中插入<omp.h>的头文件。

做完了最基本的设置之后,我们就可以开始改程序了:

#pragma omp parallel for   //就多了这么一句,在我的电脑上就快了75%;
    for ( int j = 0; j < num; j++ )
    {  
        k[j]=j;
         for(int p=0;p<10000;p++)cout(p);
    }    
    for(int i=0;i<num;i++)
       printf("%d ",k[i]);

这就可以了,我们发现,仅仅只是多了一条“#pragma omp parallel for”而已,但是就多了这么一句,就是程序在我的计算机上快了近75%(估测,非精确计算)。原先程序使用了近32秒,但是现在程序仅用了8秒,这证明了程序运行确实是大幅提高了性能。那我们该着么理解这仅多出来的一条语句呢?很简单,这显然是编译器控制指令,它会在程序编译的时候自动分析for语句,将它拆成多个线程来运行。这样做的好处是我们能够很简单的就创建多线程程序而无需去了解其底层的运行机制,但是它也有一些局限性,使得我们不得不考虑以下几个问题:

    1.对for语句我们必须使用for(i=xxx;x<或>xxx;i的运算法则不能含有变量)其中xxx为固定的常数的形式的循环才能够使用#pragma omp parallel for (当然sections结构也可以进行拆分),也就是说在需要进行多线程化的循环必须是定长的、可预见的,当然使用低层API我们可以动态的创建线程,也就不存在定长循环的问题。

    2.对所要拆分线程的for语句,规定不得存在迭代相关性。因为创建线程后线程之间是独立运行的,如果线程之间存在相关性,将会使得程序产生混乱的运行结果。当然使用底层的API我们可以更有效的控制和操作各个线程的合作与运行,但是在openmp就无法实现了。如果要进行多线程化的循环出现了迭代相关性,那我们必须重新设计for循环,以等效的无相关性的循环代替它,但是代价就是提高了程序的复杂性。

    3.数据竞争。我们必须保证,各个线程之间不会对同一个外部或公共变量进行操作(哪怕是读操作都是危险的),在此情况下,Intel提供了两种方案:一种是尽可能的使用私有或局部变量;一种是获取公共变量的副本。

    总之,openmp的编程技术确实是非常的方便与美妙,只要我们在程序设计的时候遵守了以上几个法则即可。当然,openmp的功能要远为强大的多,包括更复杂的线程分配方案、更复杂的操作等,但是却能很简单的应用。不过对本例来说还用不着那些技术,仅 #pragma omp parallel for就足亦。下面我将这次试验的源代码完整的贴上来,以使各位能够更全面的来了解一下程序的结构:

//多线程试验程序

#include "stdafx.h"
#include<stdio.h>
#include<omp.h>
#include<conio.h>

#define num 100


void cout(int p) //复杂运算函数,仅是让他做复杂计算;
{
    p=(1+2+3+4+5+6+7+8+9+10)/(9*8*7*6*5*4*3*2*1);
    for(int k=0;k<10000;k++);
}
int main(int argc, char* argv[])
{    
    int k[num];
    char c='0';
    printf("
请选择想执行的程序:\n1.单线程示例程序\n2.多线程示例程序\n3.退出\n注:这两段程序相同所不同的是第二段为多线程程序,计算性能应从电脑提示程序已经开始时计算!\n请输入:");

//这个for是整个循环主题了,也就是程序的框架;但是这个for不是最耗性能的,所以不用多线程化;
    for(;c!='3';)         //如果c=='3'的话就直接退出,但是首次运行时赋值为'0',所以不会退出;
    {
        c=_getch();         //
选择输入;
        if(c=='1')         //这个if就是第一段程序的总体了,可以与下个else if比较发现仅少了 #pragma omp parallel for 语句;
        {
             printf("1\n
第一段程序已经开始了,该段程序是单线程程序\n");
            for ( int j = 0; j < num; j++ )
             {  
                 k[j]=j;
                   for(int p=0;p<10000;p++)cout(p); //开始做无用功!既进行很大的for循环,又调用函数,主要目的就是让它耗时间;

             }    
              for(int i=0;i<num;i++)     //
顺序输出0-99;
                 printf("%d ",k[i]);
              printf("\n
第一段程序已经结束\n请输入:");
       }

       else if(c=='2')        //第二段主程序;
       {
             printf("2\n
第二段程序已经开始了,该段程序和第一段程序相同,只是多了#pragma omp parallel for,我们可以仔细观察它们的区别\n");
             #pragma omp parallel for    //就多了这么一句,在我的电脑上就快了75%;

             for ( int j = 0; j < num; j++ )
            {  
                k[j]=j;
                for(int p=0;p<10000;p++)cout(p);
           }    
            for(int i=0;i<num;i++)
                printf("%d ",k[i]);
           printf("\n
第二段程序已经结束\n请输入:");
        }

        else          //稍微做的正规点,就加了这么一句;
            printf("无该选项,请重新选过!\n请输入:");
     }
    return 0;
}

    实验的硬件平台是Intel Core2 Quad Q6600 2.4G四核处理器,实验的软件平台是:VC6.0+Intel C++编译器10.1