虽然MPICH和openMPI已经很好用,但是有时候在实际应用中还是很麻烦。比如说,两个进程之间要通信,需要发送一个vector<int>类型的数据,甚至发送一个自定义的对象。这个时候就要涉及到序列化的问题,不把一个对象转换成字节流,是没法正确的传输数据的。而且MPI的新版本也不再支持C++了,得使用C&C++混编,这着实让人难受。

本文使用C++, 以及boost 1.59

什么是Boost

Boost是一个超级厉害的C++的库,厉害到什么程度呢?C++11这次更新就吸收了大量的Boost中的内容。总而言之这个库可以看做是STL的扩展,就是一些泛型复用的工具,涉及很多方面,比如字符串文本处理,内存处理,并发编程,函数对象等等。Boost之于C++,大概相当于numpy/scipy之于python。不过Boost这个库是一个很大很复杂的生态,还是谨慎使用的好。

为什么要用Boost

如果你通过搜索引擎搜到这篇文章(大概不可能),多半现在已经对MPI有些了解了,我猜可能你大概想要用boost_serialization来序列化,毕竟手动管理内存序列化太耗精力,然后顺便配套一起使用boost_MPI
如果是友链过来的,啊哈,看看热闹就好吧。

Boost环境配置

首先,要Boost支持MPI,那就得注意带上MPI的支持,如果你的系统自带了Boost库(比如Gentoo),那么最好还是自己下一个重新编吧。但是这个Boost版本不同安装方式也有些区别,注意找对应版本的安装说明,比如1.59的.

注意MPI不是boost的默认组件,所以安装的时候需要选择配置mpi。

首先你得先装好MPICH或者openMPI。然后步骤是:

  1. 运行./bootstrap.sh
  2. 在生成的project-config.jam文件中添加(注意冒号之前的空格啊)
    1
    using mpi ;
    
  3. 运行/b2 install, (可以考虑使用--prefix=PATH来自定义安装目录)

好了,你可以用下面的命令来编译你的工程了。(这个是带serialization的版本)

1
mpic++ xxx.cpp /pathto/libboost_serialization.so /pathto/libboost_mpi.so -o xxx

还有一些配置PATHCPLUS_INCLUDE_PATH还有LD_LIBRARY_PATH的细节,我不说聪明的你也是会配置的对吧。

Boost serialization

下面的内容是教程中的一些提炼和整理,例子也修改到和boost_MPI相契合。

普通的序列化

下面这个例子会告诉你如何序列化一个int类型的值。

1
2
3
4
5
6
7
8
9
#include <boost/archive/text_oarchive.hpp> 
#include <iostream> 

int main() 
{ 
  boost::archive::text_oarchive oa(std::cout); 
  int i = 1; 
  oa << i; 
}

但是这并没有什么用,在boost_MPI中的send和recv都是可以发送基本字节单位的。
那么稍微高级一点,发送一个STL容器需要包含一个特别的头文件,然后boost_MPI会自动序列化这些容器。

1
2
3
#include <boost/serialization/vector.hpp>
#include <boost/serialization/string.hpp>
#include <boost/serialization/queue.hpp>

上面这些内容可能对你一点帮助也没有,毕竟你想要搜索解决的问题也不可能这么简单。

对象类层次结构序列化

假如是你需要发送一个类class从一个master节点广播到slave节点,所以就需要序列化这个类。一般有侵入式和非侵入式两种序列化方法,(如果你要序列化的代码不能修改,才考虑用非侵入式的方式,然而也不可能那么巧所有的成员变量都是public类型的)。这里就只介绍侵入式的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Test { 
public: 
  Test() {} 
private: 
  friend class boost::serialization::access; 
  template <typename Archive> 
  void serialize(Archive &ar, const unsigned int version) { 
    ar & value;
    ar & array; 
  } 
  int value; 
  std::vector<int> array;
};

非常简洁,你只要在private域中加上几行代码就ok了,你只要注意在serialize这个方法里依葫芦画瓢地把所有的成员变量写一边就好。然后boost_MPI就能自动的在发送时序列化这个类了。

1
2
Test t;
boost::mpi::broadcast(world, t, 0);

然而光是发送一个class也不能解决全部问题。很多时候我们需要用到继承,所以类的层次结构序列化也是需要解决的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//Base
class Test { 
public: 
  Test() {} 
private: 
  friend class boost::serialization::access; 
  template <typename Archive> 
  void serialize(Archive &ar, const unsigned int version) { 
    ar & value;
    ar & array; 
  } 
  int value; 
  std::vector<int> array;
}; 

//Derivation
class Testson:public Test{
 public:
    Testson(){}
 private:
 friend class boost::serialization::access; 
 template <typename Archive> 
 void serialize(Archive &ar, const unsigned int version) { 
   ar & boost::serialization::base_object<Test>(*this); 
   ar & son_attr; 
 } 
 int son_attr;
};

子类中的serialize稍微有点不一样。这样在boost_MPI发送的时候也会自动序列化子类。

指针和引用序列化

使用C++肯定少不了使用指针,比如最常见的使用动态联编特性,用基类指针来调用子类方法。还用上面的那个例子来讲。只需要多加上一行

1
2
#include <boost/serialization/export.hpp> 
BOOST_CLASS_EXPORT(Testson)

至于其他的基本类型,或者是简单的类,如果正确地按照上述方法序列化了,boost_MPI可以不加修改的自动的在发送时序列化对象。如果你要使用智能指针,嗯,三思而后行吧。

Boost MPI

为了更好的配合boost_serialization,使用boost_MPI是十分明智的选择。再重复一边boost_MPI在安装的时候需要自己选择编译进boost,默认安装是不包含mpi组件的。

初始化

首先是MPI的初始化,和基本的变量

1
2
3
4
5
6
7
8
#include <boost/mpi.hpp>
#include <iostream>

int main(int argc, char *argv[]){
  boost::mpi::environment env{argc, argv};
  boost::mpi::communicator world;
  std::cout << world.rank() << ", " << world.size() << '\n';
}

然而这个boost_MPI非常邪门,只有Init,没有Finalize。world.rank()表示当前进程的rank,world.size()表示当前一共开了多少个进程。

简单的P2P通信

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <boost/mpi.hpp>
#include <iostream>

int main(int argc, char *argv[]){
  boost::mpi::environment env{argc, argv};
  boost::mpi::communicator world;
  if (world.rank() == 0){
    int i;
    world.recv(boost::mpi::any_source, 16, i);
    std::cout << i << '\n';
  }
  else if (world.rank() == 1){
    world.send(0, 16, 99);
  }
}

send(发给谁,标签,发什么),recv(谁发的,标签,存到哪),就这么简单。其实还有别的参数,具体见这里,非常长,非常详细,还有源码。

异步P2P通信

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
include <boost/mpi.hpp>
#include <boost/serialization/string.hpp>
#include <string>
#include <iostream>

int main(int argc, char *argv[]){
  boost::mpi::environment env{argc, argv};
  boost::mpi::communicator world;
  if (world.rank() == 0){
    std::string s;
    boost::mpi::request r = world.irecv(boost::mpi::any_source, 16, s);
    if (r.test())
      std::cout << s << '\n';
    else
      r.cancel();
  }
  else{
    std::string s = "Hello, world!";
    world.send(0, 16, s);
  }
}

参数顺序同上,这里使用test()来测试是否接收到。如果要等待多个进程,那么使用wait_all

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <boost/mpi.hpp>
#include <boost/serialization/string.hpp>
#include <string>
#include <iostream>

int main(int argc, char *argv[]){
  boost::mpi::environment env{argc, argv};
  boost::mpi::communicator world;
  if (world.rank() == 0){
    boost::mpi::request requests[2];
    std::string s[2];
    requests[0] = world.irecv(1, 16, s[0]);
    requests[1] = world.irecv(2, 16, s[1]);
    boost::mpi::wait_all(requests, requests + 2);
    std::cout << s[0] << "; " << s[1] << '\n';
  }
  else if (world.rank() == 1){
    std::string s = "Hello, world!";
    world.send(0, 16, s);
  }
  else if (world.rank() == 2){
    std::string s = "Hello, moon!";
    world.send(0, 16, s);
  }
}

集合通信规范

十分简单,但是这些方法需要传入world而不是world的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <boost/mpi.hpp>
#include <boost/serialization/string.hpp>
#include <string>
#include <iostream>

int main(int argc, char *argv[])
{
  boost::mpi::environment env{argc, argv};
  boost::mpi::communicator world;
  std::string s;
  if (world.rank() == 0)
    s = "Hello, world!";
  boost::mpi::broadcast(world, s, 0);
  std::cout << s << '\n';
}

用法非常多,但是如果会了解MPI的话,使用肯定不是难事,所以我就不一一赘述了,具体见这里

其他

介绍一些网站,有更多更全的资料
MPI教程隔壁
boost序列化
boost MPI

by septimk at 2015.10.31