1. 程式人生 > >漫談併發程式設計:用MPI進行分散式記憶體程式設計(入門篇)

漫談併發程式設計:用MPI進行分散式記憶體程式設計(入門篇)

0x00 前言

本篇是MPI的入門教程,主要是為了簡單地瞭解MPI的設計和基本用法,方便和現在的Hadoop、Spark做對比,並嘗試理解它們之間在設計上有什麼區別。

身處Hadoop、Spark這些優秀的分散式開發框架蓬勃發展的今天,老的分散式程式設計模型是否沒有必要學習?這個很難回答,但是我更傾向於花一個下午的時候來學習和了解它。

關於併發和並行程式設計系列的文章請參考文章集合

文章結構

  1. 舉個最簡單的例子,通過這個例子讓大家對MPI有一個基本的理解。
  2. 解釋一些和MPI相關的概念。
  3. 列舉一些MPI的常用函式,以及基本用法
  4. 通過兩個例子詳細說明MPI的用法

0x01 舉個栗子

安裝

建議在Ubuntu上安裝,不過筆者嘗試一下,報了各種錯。正好Win10可以安裝一個Linux的bash,就安裝了一下,用起來和原生Linux沒什麼區別,挺方便。

一句搞定。

sudo apt-get install libcr-dev mpich2 mpich2-doc

helloworld

MPI的c語言版helloworld。這是一個最簡單的版本,相當於是每個程序都列印一下helloworld。

該例子中的一些方法以及概念在後面都會解釋,而且會有兩個比這個功能更全一點的例子來幫助理解。

#include <mpi.h>
#include <stdio.h>
int main (int argc, char* argv[]) { int rank, size; MPI_Init (&argc, &argv); /* starts MPI*/ MPI_Comm_rank (MPI_COMM_WORLD, &rank); /* get current process id*/ MPI_Comm_size (MPI_COMM_WORLD, &size); /* get number of processes*/ printf( "Hello world from process %d of %d\n"
, rank, size ); MPI_Finalize(); return 0; }

執行

先編譯,如果有for迴圈的話,記得加上後面的引數。

mpicc mpi_hello.c -o hello -std=c99

再執行:

$ mpirun -np 5 ./hello
Hello world from process 0 of 5
Hello world from process 1 of 5
Hello world from process 2 of 5
Hello world from process 3 of 5
Hello world from process 4 of 5

總結

從上面的簡單例子可以看出 一個MPI程式的框架結構可以用下圖表示 把握了其結構之後,下面的主要任務就是掌握MPI提供的各種通訊方法與手段。

安裝時遇到的問題

來一個我在Ubuntu16.04下遇到的錯誤,實在不想解決這些亂七八糟的,就跳過了。

$sudo apt-get install libcr-dev mpich2 mpich2-doc
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Package mpich2 is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
However the following packages replace it:
  mpich:i386 mpich

Package mpich2-doc is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
However the following packages replace it:
  mpich-doc

E: Package 'mpich2' has no installation candidate
E: Package 'mpich2-doc' has no installation candidate

0x02 基本概念

什麼是MPI

對MPI的定義是多種多樣的,但不外乎下面三個方面,它們限定了MPI的內涵和外延:

  • MPI 是一個庫,不是一門語言。MPI 提供庫函式/過程供 C/C++/FORTRAN 呼叫。
  • MPI 是一種標準或規範的代表,而不特指某一個對它的具體實現。
  • MPI 是一種訊息傳遞程式設計模型。最終目的是服務於程序間通訊這一目標

名詞和概念

程式程式碼:

這裡的程式不是指以檔案形式存在的原始碼、可執行程式碼等,而是指為了完成一個計算任務而進行的一次執行過程。

程序(Process)

一個 MPI 並行程式由一組執行在相同或不同計算機 /計算節點上的程序或執行緒構成。為統一起見,我們將 MPI 程式中一個獨立參與通訊的個體稱為一個程序。

程序組:

一個 MPI程式的全部程序集合的一個有序子集。程序組中每個程序都被賦予一個在改組中唯一的序號(rank),用於在該組中標識該程序。序號範圍從 0 到程序數-1。

通訊器(communicator):

有時也譯成通訊子,是完成程序間通訊的基本環境,它描述了一組可以互相通訊的程序以及它們之間的聯接關係等資訊。MPI所有通訊必須在某個通訊器中進行。通訊器分域內通訊器(intracommunicator)和域間通訊器(intercommunicator)兩類,前者用於同一程序中程序間的通訊,後者則用於分屬不同程序的程序間的通訊。

MPI 系統在一個 MPI 程式執行時會自動建立兩個通訊器:一個稱為 MPI_COMM_WORLD,它包含 MPI 程式中所有程序,另一個稱為MPI_COMM_SELF,它指單個程序自己所構成的通訊器。

序號(rank):

即程序的標識,是用來在一個程序組或一個通訊器中標識一個程序。MPI 的程序由程序組/序號或通訊器/序號唯一確定。

訊息(message):

MPI 程式中在程序間傳遞的資料。它由通訊器、源地址、目的地址、訊息標籤和資料構成。

通訊(communication):

通訊是指在程序之間進行訊息的收發、同步等操作。

0x02 MPI核心介面

用過Hadoop的童鞋應該都記得經典的Map和Reduce介面,我們在寫MR程式的時候主要就在寫自己實現的Map和Reduce方法。

MPI比Hadoop需要關注的稍微多一點點。

注意: 這幾個核心的介面還是要了解一下的。暫時可以看一眼跳過去,後面在看程式的時候回過頭多對比一下就能記住了。

我們簡單地理解一下這6個介面,其實可以分為3類:
1. 開始和結束MPI的介面:MPI_InitMPI_Finalize
2. 獲取程序狀態的介面:MPI_Comm_rankMPI_Comm_size
3. 傳輸資料的介面:MPI_SendMPI_Recv

關於傳輸資料的介面,可以看下圖的理解。

1. MPI_Init(&argc, &argv)

初始化MPI執行環境,建立多個MPI程序之間的聯絡,為後續通訊做準備。

2. MPI_Comm_rank(communicator, &myid)

用來標識各個MPI程序的,給出呼叫該函式的程序的程序號,返回整型的錯誤值。兩個引數:MPI_Comm型別的通訊域,標識參與計算的MPI程序組; &rank返回呼叫程序中的標識號。

3. MPI_Comm_size(communicator, &numprocs)

用來標識相應程序組中有多少個程序。

4. MPI_Finalize()

結束MPI執行環境。

5. MPI_Send(buf,counter,datatype,dest,tag,comm)

  • buf:傳送緩衝區的起始地址,可以是陣列或結構指標;
  • count:非負整數,傳送的資料個數;
  • datatype:傳送資料的資料型別;
  • dest:整型,目的的程序號;
  • tag:整型,訊息標誌;comm:MPI程序組所在的通訊域

含義:向通訊域中的dest程序傳送資料,資料存放在buf中,型別是datatype,個數是count,這個訊息的標誌是tag,用以和本程序向同一目的程序傳送的其它訊息區別開來。

6. MPI_Recv(buf,count,datatype,source,tag,comm,status)

  • source:整型,接收資料的來源,即傳送資料程序的程序號;
  • status:MPI_Status結構指標,返回狀態資訊。

0x03 進階版HelloWorld

在這裡舉第二個例子——一個進階版的HelloWorld。不再像第一個例子那樣簡單地列印HelloWorld,在這個程式中,我們指派其中一個程序複雜輸出,其它的程序向他傳送要列印的訊息。

程式

在這個程式中,為了方便理解我會註釋大部分的程式碼。

注意註釋。


#include <stdio.h>
#include <string.h>  /* For strlen             */
//MPI相關的庫
#include <mpi.h>     /* For MPI functions, etc */

const int MAX_STRING = 100;

int main(void) {
   char       greeting[MAX_STRING];  /* String storing message*/
   int        comm_sz;               //程序數
   int        my_rank;               //當前程序的程序號

   //初始化MPI
   MPI_Init(NULL, NULL);

   //獲取程序的數量,並存入comm_sz變數中
   MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);

   //獲取當前程序的程序號
   MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

   // 程序號不為0的處理邏輯。
   // 在該程式中,程序號不為0的程序,只負責發資料給程序0。
   if (my_rank != 0) {
      //建立要傳送的資料
      sprintf(greeting, "Greetings from process %d of %d!",
            my_rank, comm_sz);
      //傳送資料給程序0
      MPI_Send(greeting, strlen(greeting)+1, MPI_CHAR, 0, 0,
            MPI_COMM_WORLD);
   }
   // 程序號為0的處理邏輯
   else {  
      // 列印程序0的資料
      printf("Hello! Greetings from process %d of %d!\n", my_rank, comm_sz);
      // 迴圈接收其它程序傳送的資料,並列印。
      for (int q = 1; q < comm_sz; q++) {
         // 接收其它程序的資料
         MPI_Recv(greeting, MAX_STRING, MPI_CHAR, q,
            0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
         // 列印
         printf("%s\n", greeting);
      }
   }

   // 關閉MPI
   MPI_Finalize();

   return 0;
}  /* main */

執行

看一下執行結果。

第一條是我們的程序0列印的,其餘的4條都是接收其它程序的資料。

$ mpicc mpi_hello.c -o hello  -std=c99
$ mpirun -np 5 ./hello
Hello! Greetings from process 0 of 5!
Greetings from process 1 of 5!
Greetings from process 2 of 5!
Greetings from process 3 of 5!
Greetings from process 4 of 5!

0x04 MPI實現梯形積分法

問題描述

用梯形積分法來估計函式 y=f(x) 的影象中,兩條垂直線與x軸之間的區域大小。即下圖(1)中陰影部分面積。

解法

如上圖中的(b),基本思想就是,將x軸的區間劃分為n個等長的子區間,然後估計每個子區間範圍內的圖形面積。最後相加即可。(當然了,這是估算的面積)

其中,梯形的面積如下:

梯形面積= h/2 * (f(xi) + f(xi+1))

其中高h是我們等分的一個區間值,h=(b-a)/n

那麼整個圖形的面積如下:

序列程式

根據上面的公式,我們可以得到一個序列版的程式:

h = (b-a)/n
approx = (f(a) + f(b)) /2.0
for(i=1;i<n-1;i++):
    x_i = a + i*h
    approx += f(x_i)
approx = h*approx

MPI程式

整個MPI程式設計如下:

  • 程序1~n, 負責各自的矩形面積
  • 程序0,負責將所有矩形面積加起來求和

如下圖

對應到程式碼如下:

int main(void) {
   int my_rank, comm_sz, n = 1024, local_n;   
   double a = 0.0, b = 3.0, h, local_a, local_b;
   double local_int, total_int;
   int source;

   MPI_Init(NULL, NULL);
   MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
   MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);

   // 區間大小,所有程序一樣
   h = (b-a)/n;        
   local_n = n/comm_sz;  // 每個processor需要處理的梯形的數目

   /* Length of each process' interval of
    * integration = local_n*h.  So my interval
    * starts at: */
   local_a = a + my_rank*local_n*h;
   local_b = local_a + local_n*h;
   local_int = Trap(local_a, local_b, local_n, h);

   // 將所有的程序的結果相加
   if (my_rank != 0) {
      MPI_Send(&local_int, 1, MPI_DOUBLE, 0, 0,
            MPI_COMM_WORLD);
   }
   // 每個程序單獨計算梯形面積
   else {
      total_int = local_int;
      for (source = 1; source < comm_sz; source++) {
         MPI_Recv(&local_int, 1, MPI_DOUBLE, source, 0,
            MPI_COMM_WORLD, MPI_STATUS_IGNORE);
         total_int += local_int;
      }
   }

   // 程序0列印結果
   if (my_rank == 0) {
      printf("With n = %d trapezoids, our estimate\n", n);
      printf("of the integral from %f to %f = %.15e\n",
          a, b, total_int);
   }
   MPI_Finalize();

   return 0;
} /*  main  */

Trap是梯形積分法的序列實現。供每一個processor呼叫。

double Trap(
      double left_endpt  /* in */,
      double right_endpt /* in */,
      int    trap_count  /* in */,
      double base_len    /* in */) {
   double estimate, x;
   int i;

   estimate = (f(left_endpt) + f(right_endpt))/2.0;
   for (i = 1; i <= trap_count-1; i++) {
      x = left_endpt + i*base_len;
      estimate += f(x);
   }
   estimate = estimate*base_len;

   return estimate;
}

結果

$mpirun -np 6 ./trap
With n = 1024 trapezoids, our estimate
of the integral from 0.000000 to 3.000000 = 8.894946975633502e+00

0xFF 總結

趁著端午假期的一個下午,把MPI做了一個小的總結。 程度不深,主要是瞭解MPI的一些基本特性。

暫時總結到這裡,後續的工作和學習中如果再遇到了和MPI相關的知識點,再繼續深入。

完整程式碼請看github地址。

參考

個人主頁:http://dantezhao.com
文章可以轉載, 但必須以超連結形式標明文章原始出處和作者資訊