多线程算法(—)

用于学习和交流,欢迎指正。

多线程算法

                                                                                      ——算法导论第 3 版新增第 27

 

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein

 

邓辉

 

原文: http://software.intel.com/sites/products/documentation/cilk/book_chapter.pdf

 

       本书中的主要算法都是顺序算法 ,适合于运行在每次只能执行一条指令的单处理器计算机上。在本章中,我们要把算法模型转向并行算法 ,它们可以运行在能够同时执行多条指令的多处理器计算机中。我们将着重探索优雅的动态多线程算法模型,该模型既有助于算法的设计和分析,同时也易于进行高效的实现。

 

       并行计算机(就是具有多个处理单元的计算机)已经变得越来越常见,并且在价格和性能方面差距甚大。相对比较便宜的有片上多处理器 桌面电脑和笔记本电脑,其中包含着一个多核集成芯片,容纳着多个处理“核”,每个核都是功能齐全的处理器,可以访问一个公共内存。价格和性能都处于中间的是由多个独立计算机(通常都只是些 PC 级的电脑)组成的集群,通过专用的网络连接在一起。价格最高的是超级计算机,它们常常采用定制的架构和网络以提供最高的性能(每秒执行的指令数)。

 

       多处理器计算机已经以各种形态存在数十年了。计算社团早在计算机科学形成的初期就选定采用随机存取的机器模型来进行串行计算,但是对于并行计算来说,却没有一个公认的模型。这主要是因为供应商无法在并行计算机的架构模型上达成一致。比如,有些并行计算机采用共享内存 ,其中每个处理器都可以直接访问内存的任何位置。而有些并行计算机则使用分布式内存 ,每个处理器的内存都是私有的,要想去访问其他处理器的内存,必须得向其他处理器发送显式的消息。不过,随着多核技术的出现,新的笔记本和桌面电脑目前都成为共享内存的并行计算机,趋势似乎倒向了共享内存多处理这边。虽然一切还是得由时间来证明,不过我们在章中仍将采用共享内存的方法。

 

       对于片上多处理器和其他共享内存并行计算机来说,使用静态线程 是一种常见的编程方法,该方法提供了一种共享内存的“虚拟处理器”或者线程的软件抽象。每个线程维持着自己的程序计数器,可以独立地执行代码。操作系统把线程加载到一个处理器上让其运行,并在其他线程需要运行时将其换下。操作系统允许程序员创建和销毁线程,不过这些操作的开销较大。因此,对于大多数应用来说,在计算期间线程是持久存在的,这也是为何称它们为“静态”的原因。

 

       遗憾的是,在共享内存并行计算机上直接使用静态线程编程非常的困难且易于出错。原因之一为,为了使每个线程所承担的负载大致相当,就需要动态地在线程间分配工作,而这是一项极其复杂的任务。除了那些最简单的应用之外,程序员都得使用复杂的通信协议来实现调度器以对工作进行均衡。这种状况导致了并发平台 的出现,并发平台就是一个用来协调、调度、管理并行计算资源的软件平台。有些并发平台被构建成运行时库,有些则提供了具有编译器和运行时支持的全功能的并行语言。

 

动态多线程编程

       动态多线程 是一种重要的并发平台,也是本章中要采用的模型。使用动态多线程平台,程序员可以无需关心通信协议、负载均衡以及其他静态线程编程中的复杂问题,只要明确应用中的并行性即可。该并发平台中有一个调度器,用来自动均衡计算的负载,因此大大地简化了程序员的工作。动态多线程环境所具有的功能目前还在不断的演化之中,不过有两个功能是必备的:嵌套并行以及并行循环。嵌套并行就是可以去“ spawn ”一个子例程,并且使得调用者和子例程能够同时执行。并行循环和普通的 for 循环相似,只是循环中的迭代可以并发地执行。

 

       这两个功能是我们将要在本章中研究的动态多线程模型的基础。该模型的一个关键特征为,程序员只需要指明计算中的逻辑并行性,底层并发平台中的线程会自动调度和均衡计算。我们将研究基于这种模型所编写的多线程算法,以及底层并发平台能够高效进行计算调度的原理。

 

       动态多线程模型具有如下几个重要优点:

l         它是串行编程模型的简单扩展。只需在伪码中增加 3 个“并发”关键字: parallel spawn 以及 sync ,就可以描述多线程算法。此外,如果从多线程伪码中去掉这些关键字,就可以得到针对相同问题的串行伪代码,我们称之为“串行化”一个多线程算法。

l         它提供了一种理论上清晰的、基于“ work (工作量)”和“ span (跨度)”这两个概念量化 parallelism (并行度)的方法。

l         许多多线程算法所涉及的嵌套并行可以从分治范型自然得出。此外,正如串行分治算法可以容易地通过递归关系进行分析一样,多线程算法也是如此。

l         该模型符合并行计算实践的演化方向。越来越多的并发平台开始支持动态多线程技术的不同变种,包括 Cilk [51, 118], Cilk++ [72], OpenMP [60], Task Parallel Library [230], and Threading Building Blocks [292]

 

       27.1 小节中,我们会介绍动态多线程模型,以及有关 work span 以及 parallelism 的度量方法,我们会使用该度量去分析多线程算法。在 27.2 小节中,我们将研究如何使用多线程来进行矩阵相乘,在 27.3 小节中,我们将处理一个更为困难的问题:归并排序的多线程算法

27.1 动态多线程技术基础

       我们将以递归地计算 Fibonacci 数为例来开始对于动态多线程技术的探索历程。先来回忆一下 3.22 小节中给出的 Fibonacci 数的递归定义:

       F0 = 0,

       F1 = 1,

       Fi = Fi-1 + Fi-2  for i 2.

 

       下面是一个简单的用于计算第 n Fibonacci 数的递归串行算法:

       FIB(n)

       1  if n 1

2      return n

3  else x = FIB(n-1)

4      y = FIB(n-2)

5      return x+y

 

如果要计算很大的 Fibonacci 数,是不能使用该算法的,因为其中有大量的重复计算。图 27.1 展示了在计算 F6 时所创建的递归过程实例树。其中,对于对于 FIB(6) 的调用会递归地调用 FIB(5) FIB(4) 。而对 FIB(5) 的调用又会去调用 FIB(4) 。这两个 FIB(4) 实例返回的结果完全相同( F4 =3 )。由于 FIB 并没有去记住这些结果,因此对于 FIB(4) 的第二次调用重复了第一次调用的工作。

      

       我们用 T(n) 表示 FIB(n) 的运行时间。由于 FIB(n) 包含了两个递归调用和其他一些常数时间的工作,因此得到如下递归方程:

 

       T(n) = T(n-1) + T(n-2) + Θ(1)

 

       我们可以采用替换方法得到该方程的解: T(n) = Θ( Fn) 。作为归纳假设,我们假设 T(n ) aFn-b ,其中a>1b>0 且都为常数。通过替换,我们得到:

 

       T(n) ≤ (aF n-1 -b + (aF n-2 -b + Θ(1)

         =   a( F n-1 + F n-2 - 2b + Θ(1)

         =  aF n -b – (b- Θ(1))

          aF n -b

 

    如果我们在选择b 时让其大到足以支配 Θ(1) 中的常量。那么我们接着可以把a 选得大到足以满足初始条件。其分析边界为:

 

    T(n) = Θ( Φn ),                                       (27.1)

 

       其中, Φ=(1+sqrt(5))/2 是黄金分割率,由等式(3.25) 得出。由于Fn n 成指数级增长,因此在计算 Fibonacci 数时,该过程非常低效。(问题 31-3 中给出了快得多的方法)。

 

       虽然上面的 FIB 过程对计算 Fibonacci 数来说是一种糟糕的方法,但是在说明多线程算法分析中的关键概念方面,它却是一个好例子。在 FIB(n) 中,第 3 行、第 4 行中对 FIB(n-1) FIB(n-2) 的两个递归调用彼此之间相互独立:它们可以按照任意顺序被调用,相互之间也不会有任何影响。因此,这两个递归调用是可以并行运行的。

 

       我们在伪代码中增加了并发关键字 spawn sync 来指示并行属性。下面是采用动态多线程技术重写的 FIB 过程:

P-FIB(n)

       1  if n 1

2      return n

3  else x = spawn P-FIB(n-1)

4      y = P-FIB(n-2)

5      sync

6      return x+y

 

       请注意,如果我们从 P-FIB 中删除掉并发关键字 spawn sync ,剩下的代码和 FIB 完全一样(除了开始和两处递归调用处的过程名字被更改之外)。我们把多线程算法的串行化 定义为:删除了多线程关键字 spawn sync 以及 parallel (并行循环中会用到)后所得到的串行算法。事实上,我们的多线程伪代码具有一个不错的属性——其串行化版本就是解决相同问题的常用串行伪代码。

 

       在过程调用前面加上 spawn 关键字时,就意味着嵌套并行 ,如第 3 行中所示。 spawn 的语义和普通的过程调用不同,执行 spawn 的过程实例( parent )可以和被 spawn 出来的子例程( child )并行执行,而不像串行执行中那样去等待 child 执行完成。在本例中,当 child 在计算 P-FIB(n-1) 时, parent 可以并行地去计算第 4 行中的 P-FIB(n-2) 。由于 P-FIB 过程是递归的,因此这两个对其自身进行调用的子例程就创建了嵌套的并行性,对其 children 来说同样如此,于是就产生了一个潜在的巨大子计算树,每个子计算都并行执行。

 

       不过,关键字 spawn 并不是一定要求过程必须和其 child 并发执行,只是表示可以 并发执行。并发关键字表达了计算中的逻辑并行性 ,表明了计算中的哪些部分可以并行的运行。哪些子计算实际上是并发运行的是由调度器 在运行时决定的,在计算进行中,调度器把子计算分配给可用的处理器。稍后,我们会讨论调度器的原理。

 

       一个过程,仅当其执行了 sync 语句时(如第 5 行),才能够安全地使用由其 spawn children 例程的返回值。关键字 sync 表示过程必须等待,直到其所 spawn children 全部完成计算,才能够继续 sync 后面的语句。在 P-FIB 过程中,必须要在 return 语句(第 6 行)前增加 sync 语句,从而避免出现在 x 还没有被计算前就进行 x+y 操作的异常情况。除了 sync 语句所提供的显式同步之外,每个过程都会在其返回前隐式地执行一条 sync 语句,这样可以保证在其终止前,其所有的 children 都已经终止。

 

多线程执行模型

       把多线程计算(由一个代表多线程程序的处理器执行的运行时指令集)看做是一个有向无环图 G= V, E )是很有帮助的,我们称其为计算 dag directed acyclic graph 。图 27.2

中给出了一个示例,其中的计算 dag 来自计算 P-FIB(4) 。从概念层面来讲, V 中的顶点都是指令, E 中边则表示指令间的依赖关系, (u,v ) E 表示指令 u 必须在指令 v 之前执行。为了方便起见,如果一条指令链中不包含任何并行控制语句(没有 spawn sync 以及被 spawn 例程中 return ——显式的 return 语句或者过程执行完后的隐式 return ),我们就把它们组成一组,形成一个 strand ,每个 strand 都表示一条或者多条指令。涉及并行控制的指令不包括在 strand 中,但是会出现在 dag 结构中。例如,如果一个 strand 有两个后继,那么其中之一必须得被 spawn 出来,如果一个 strand 有多个前驱,就表示前驱因为一条 sync 语句被合并在一起。因此,一般来说, V 形成了 strand 集合,而有向边集合 E 则表示由并行控制产生的 strand 间的依赖关系。如果 G 中有一个从 strand u strand v 的有向路径,那么我们就说这两个 strands 是(逻辑上)串行的。否则就称其为(逻辑上)并行的。

 

       我们可以把一个多线程计算表示为由内嵌于一棵过程实例树中的 strands 组成的有向无环图。比如,图 27.1 中展示了 P-FIB(6) 的过程实例树,其中没有显示 strands 细节。图 27.2 放大了该树中的一个片段,展现了构成每个过程的 strands 。所有连接 strands 的有向边要么运行于一个过程之中,要么沿着过程树中的无向边运行。

 

       我们可以把计算 dag 的边进行分类,以表示出不同 strands 间依赖关系的种类。在图 27.2 中,沿水平方向连接 strand u 和其同一个过程实例中的后继 u’ 的边被称为 continuation edage (继续边) u u’ )。当 strand u spawn strand v 时, dag 中就包含了另一个 spawn edge (u,v) ,在图中显示为指向下的边。表示正常过程调用的 call edge 也指向下。 Strand u spawn strand v u 调用 v 的差别在于: spawn 会产生一条从 u 到其同一过程中后继 u’ 的水平方向的 continuation edge ,意味着 u’ 可以和 v 同时执行,而调用不会产生出这样的边。当 strand u 返回到其调用过程,而 x 是该调用过程中紧跟着下一条 sync 语句的 strand 时,计算 dag 中就会包含 return edge (u,x) ,指向上方。计算从一个 initial strand 开始执行(图 27.2 中被标记为 P-FIB(4) 的过程中的黑色顶点),并以一个 final strand 结束(被标记为 P-FIB(4) 的过程中的白色顶点)。

 

       我们将在理想并行计算机 上研究并行算法的执行,该理想并行计算机由一组处理器和一个顺序一致性 的共享内存组成。顺序一致性的意思是,虽然在实际上多个处理器可以同时对共享内存执行众多的存取操作,但是其产生的结果和在每一步中都只有来自一个处理器的一条指令被执行所产生的完全一样。也就是说,内存的行为就像是按照某个全局的线性顺序来执行指令,该全局顺序保证了每个处理器基于独立的顺序来发出自己的指令。对于动态多线程计算来说,计算是被并发平台自动调度到处理器上的,共享内存的工作方式看起来就像是多线程计算的指令相互交织形成了一个线性的顺序来保持计算 bag 中的偏序关系。这个顺序和调度有关,因此在程序的每次运行可能互不相同,但是每次运行时,我都可以假设指令是按照和计算 bag 一致的某个线性顺序执行的,并基于这个假设来理解其行为。

 

       除了对语义进行假设外,还可以对理想并行计算机模型做一些性能方面的假设。特别地,我们假设机器中的每个处理器具有相同的计算能力,并忽略掉调度的开销。虽然后面这个假设听起来过于乐观,不过在实践中,对于具有充分“ parallelism (并行度)”(后面会准确定义这个术语)的算法来说,调度的开销通常是极其小的。

性能度量

(未完待续)

      

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
1. 设计目的、意义(功能描述) 蒙特·卡罗方法(Monte Carlo method),也称统计模拟方法,是二十世纪四十年代中期由于科学技术的发展和电子计算机的发明,而被提出的一种以概率统计理论为指导的一类非常重要的数值计算方法。本次大作业主要是对蒙特·卡罗方法进行并行处理,通过OpenMP、MPI、.NET、Java、Win32API等一系列并行技术和并行机制对该算法进行并行处理,从而也进一步熟悉了蒙特·卡罗方法的串行算法并行算法,实现了用蒙特·卡罗方法计算出半径为1单位的球体的体积,体会到了并行技术在实际生活中的应用。 2. 方案分析(解决方案) 蒙特·卡罗方法(Monte Carlo method)是指使用随机数(或更常见的伪随机数)来解决很多计算问题的方法。球的体积可以估算为:位于点模型内随机点个数与全体随机点个数的比值乘以包围盒的体积算的。 3. 设计分析 3.1 串行算法设计 假定球体用B表示,半径r=1单位,B1是包含B的参考立方体(在本例中是边长为2的正方体),在B1中产生N个均匀分布的伪随机点。对每个随机点检测其是否在B内,假设位于B内的随机点个数为N(in)(<=N),应用蒙特卡洛算法,则B的体积为 V=V1(N(in)/N) 其中V1是B1的体积。如果产生足够多的随机点,理论上可以获得任意逼近精度。 算法描述如下: BEGIN N=_MAX; FOR I=0;I<_MAX;I++ X=RANDOM(); Y=RANDOM(); Z=RANDOM(); IF (X*X+Y*Y+Z*Z)<=1 COUNT++; END IF; END FOR; BULK=V1*(COUNT/_MAX); END; 本算法主要是在参考立方体的选取上和定义的_MAX的值对结果影响较大,所以应该选择合适的数。 3.2 并行算法设计 对FOR循环进行划分使用两个处理器完成计算。例如对一个长为n的序列,首先划分得到两个长为n/2的序列,将其交给两个处理器分别处理;而后进一步划分得到四个长为n/4的序列,再分别交给四个处理器处理;如此递归下去最终得到结果。当然这是理想的划分情况,如果划分步骤不能达到平均分配的目的,那么结果的效率会相对较差。 伪代码如下: BEGIN N=_MAX; FOR1 I=0;I<_MAX/2;I++ X1=RANDOM(); Y1=RANDOM(); Z1=RANDOM(); IF (X1*X1+Y1*Y1+Z1*Z1)<=1 COUNT1++; END IF; END FOR1; FOR2 I=_MAX/2+1;I<_MAX;I++ X2=RANDOM(); Y2=RANDOM(); Z2=RANDOM(); IF (X2*X2+Y2*Y2+Z2*Z2)<=1 COUNT2++; END IF; END FOR2; BULK=V1*((COUNT1+ COUNT2)/_MAX); END; 3.3 理论加速比分析 实验中大量数据所产生的加速比比小量数据所产生的加速比要体现得更明显,并且数据生成的并行加速比随着处理器核的增加而增加。设处理器个数为p,数据量为n,由于正常情况下该快速排序算法的复杂度为O(nlogn),并行处理的时间复杂度为O(klogk),其中k=n/p,所以并行算法的时间复杂度为O((n/p)log(n/p)),理论加速比为nlogn/((n/p)log(n/p))=p+logp. 4. 功能模块实现与最终结果分析 4.1 基于OpenMP的并行算法实现 4.1.1 主要功能模块与实现方法 利用了OpenMP里面的#omp parallel sections将对两个for循环用两个线程并行化执行,以多线程方式并行运行程序,并行的算法步骤如下: (1)初始化_max = 10000000; (2)创建两个线程; (3)由OpenMP编译指导语句控制产生并行执行代码区段; (4)将数据存放到tianqing_count; (5)各线程调用算法得出结果; 并行算法的部分代码如下: #pragma omp parallel for private(tianqing_x,tianqing_y,tianqing_z) reduction(+:tianqing_count2) for (tianqing_i = 0; tianqing_i<tianqing_max; tianqing_i++) { tianqing_x = rand(); tianqing_x = tianqing_x / 32767; tianqing_y = rand(); tianqing_y = tianqing_y / 32767; tianqing_z = rand(); tianqing_z = tianqing_z / 32767; if ((tianqing_x*tianqing_x + tianqing_y*tianqing_y + tianqing_z*tianqing_z) work1.pSumto(b, 0, MAXN - 1)); Thread newthread1 = new Thread(thread1); 创建Work类的对象work2; ThreadStart thread2 = new ThreadStart(() => work2.pSumto(c, 0, MAXN - 1)); Thread newthread2 = new Thread(thread2); stopwatch.Start(); 启动线程1和线程2; 等待进程结束; stopwatch.Stop(); 得到结果; 4.5.2 实验加速比分析 实验中创建了两个线程,通过多次测试,得出实验结果:由上面的理论加速比分析可知,当线程数为2时,理论加速比为2+log2=3.但由于实际操作中硬件设备以及内存分配的影响,实验加速比达不到理论值3.实验加速比在2.6~2.7左右。 4.6 并行计算技术在实际系统中的应用 4.6.1 主要功能模块与实现方法 该飞机订票系统主要实现了对机票的一些基本信息进行存储和管理的功能。在系统中实现了对机票信息的增删改查,考虑到查询的方便性,对机票按照航班号进行排序,而此排序方法用并行快速排序运用进来。利用OpenMP的并行技术,对机票信息按顺序排列好,并分析了实验过程中的加速比。 4.6.2 实验加速比分析 实验中创建了两个线程,通过多次测试,得出实验结果:当数据量比较大时,加速比理论在1.9左右。数据量较大时体现出来的加速比更准确。由上面的理论加速比分析可知,当线程数为2时,理论加速比为2+log2=3.但由于实际操作中硬件设备以及内存分配的影响,实验加速比达不到理论值3.实验加速比在2.2~2.4左右。 5. 设计体会 虽然没有按时完成作业,但这份报告花了我好几天的时间,从开始的搭建并行计算平台到最后的程序运行成功可以说是对我的一个锻炼。每一次的遇到问题与每一次的解决问题都是一个成长。每一次遇到问题和解决问题都是一种锻炼,一种尝试,从我们上并行计算课我懂得了很多电脑硬件和软件的知识,这些可能对于我们这个专业以后都是没有机会接触的,所以我觉得选择了并行计算与多核多线程技术这门课是非常正确的。对OpenMP、MPI、WIN32API、Java、.NET的并行技术有了一定的了解。在搭建MPI并行程序这块,学习的知识尤为增加,这些都是在不断的摸索、学习中学会的。 这次的大作业虽然是对以前实验的整合,但它加深了我对并行计算的印象,也使我对并行计算知识的理解更加深刻,也使我认识到了自己很多不足之处。学习并行计算的历程不会因为完成本次大作业而停止,我们是为了用知识武装大脑而学习,通过学习充实自己的生活,要努力学习,争取以后能够完成规模更大的程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值