在分布消息传递型多处理器系统中,假设有64个处理器,通过某种互连网络相互连接。进行上述求和运算的第一步是进行数据划分,将数据子集分发到各局部存储器中,数据子集存储在各局部存储器中后成为数组A1。计算步骤是先计算部分和,然后对部分和计算总和,程序为:
sum="0";
for(i=0;i<1000;i=i+1) /* loop over each array */
sum=sum+A1[i]; /* sum the local arrays */
limit=64;
half=64; /* 64 processors in this MIMD */
do {
half=half/2; /* send vs. receive dividing line */
if(Pn>=half && Pn if(Pn limit=half; /* upper limit of senders */
} while (half>1); /* exit with final sum */
该程序同时运行在多个进程或处理器中,因为都是在不同的地址空间访问数据,通过消息发送操作传递数据。程序的前三行计算部分和,后七行对部分和进行求和。为了汇集各个进程的部分和,这里程序将所有的处理器分成发送方和接收方,编译程序需要将send函数转换成对发送消息的库函数调用,将 receive函数转换成对接受消息的库函数调用。
并行程序的实现
并行程序中,任务的划分可以由程序员完成,也可以由编译程序完成。一般而言,实现并行程序设计的方法有3种:
1. 库函数。库函数方法在串行程序的标准库的基础上增加新的支持并行操作的函数。如MPI的消息传递库和POSIX的Pthread多线程库。库函数方法比较容易实现,不需要改变编译程序。但是程序中的并行性问题没有经过编译程序的检查、分析、优化。程序设计的难度较大,容易出现错误。
2. 新的语言构造。在原有的程序设计语言中增加新的构造语句,以表达并行的操作。在对并行构造语句进行分析的时候,编译程序可以进行检查,发现并行运行中存在的问题和错误。但是这种方法需要采用新的编译程序,并且增加了编译程序的复杂性。
3. 编译指导。在原有的语言中增加表达并行运算的编译指导语句。编译指导语句能够被并行编译程序识别,串行编译程序则忽略这些语句。并行编译程序跟据这些指导语句将相关代码转换成在并行计算机中运行的代码。这种方法的例子如OpenMP。这种方法的特点是介于上述两种方法之间。
多线程的编程需要利用操作系统或者软件开发环境提供的库函数实现线程操作,线程操作函数包括线程的创建和消除、线程的启动运行和终止运行、线程同步机制的建立和删除、线程互斥机制的建立和删除、临界区机制的建立和删除等。最常见的创建新进程或者新线程的机制是分叉和汇集(fork-join),调用fork函数创建一个新的进程或线程并制定进程或线程的执行入口,调用join函数则可以结束新进程或新线程的执行。在并行线程的程序设计中,程序员需要安排线程的创建、线程的同步和线程间的通信等,需要考虑到同一段代码将在多个线程中同时运行而不能相互影响。因此,在采用库函数方式下,多线程程序设计是对程序员的一个挑战。
一般情况下,在消息传递型多处理器系统中,每个处理器执行的是各不相同的程序。每个结点上运行的进程或线程的程序代码都要由程序员分别编写,进程和线程之间的通信通过MPI消息传递函数库实现。程序员需要安排好每个结点的程序中消息的发送和对应的接收函数的调用,否则就会造成处理器间的等待,甚至死机。因此程序设计的工作量很大,而且程序的调试工作十分复杂。
多线程的并行运行可以在单个处理器上进行,形成软件多线程;也可以在支持多线程的处理器核、多个处理器核或者多个处理器芯片上运行,形成硬件多线程。多线程的运行情况可以用线程运行状态时空图来表示。图1是较宏观的线程时空图的一个例子。图中用不同的灰度的条形图表示线程所处的不同状态。其中,线程通信时间包括线程等待锁的时间、同步操作花费的时间和消息发送与接收的时间。在软件多线程方式下,各个线程轮流地使用处理器核资源。这一点在线程运行状态时空图上可以清楚地看到,同一个核上的线程运行的状态相互不会出现重叠的情况,线程之间的通信也不是通过互连网络直接进行的,而是通过缓存空间进行。在硬件多线程中,运行在多个逻辑处理器上的多线程具有不同的硬件资源,构成真正的并行运行方式。
