Die Boost C++ Bibliotheken

Einfacher Datenaustausch

Boost.MPI ist eine C++-Schnittstelle zum MPI-Standard. Die Bibliothek stellt Klassen und Funktionen bereit, die alle im Namensraum boost::mpi definiert sind. Sie müssen lediglich die Headerdatei boost/mpi.hpp einbinden, um auf alle Klassen und Funktionen der Bibliothek Zugriff zu erhalten.

Beispiel 47.1. MPI-Umgebung und Kommunikator
#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';
}

Beispiel 47.1 zeigt Ihnen ein erstes einfaches MPI-Programm. In diesem Programm werden zwei Klassen verwendet, die Sie in allen folgenden Beispielprogrammen immer wieder antreffen werden. boost::mpi::environment verwenden Sie, um MPI zu initialisieren. So ruft der Konstruktor dieser Klasse die Funktion MPI_Init() aus dem MPI-Standard auf, der Destruktor MPI_Finalize(). boost::mpi::communicator wiederum verwenden Sie, um einen Kommunikator zu erstellen. Der Kommunikator ist eines der zentralen Konzepte von MPI und ermöglicht einen einfachen Datenaustausch zwischen Prozessen.

boost::mpi::environment ist eine sehr einfache Klasse, die nur wenige Methoden anbietet. So können Sie zum Beispiel über initialized() herausfinden, ob MPI erfolgreich initialisiert wurde. Die Methode gibt ein Ergebnis vom Typ bool zurück. processor_name() gibt den Namen des aktuellen Prozesses als std::string zurück. Und über abort() können Sie ein MPI-Programm stoppen – also nicht nur den aktuellen Prozess. Sie übergeben dieser Methode als einzigen Parameter einen int-Wert, der der Rückgabewert des Programms an die MPI-Laufzeitumgebung darstellt. Für die meisten MPI-Programme werden Sie diese Methoden aber nicht benötigen. Üblicherweise instanziieren Sie boost::mpi::environment zum Programmanfang und greifen danach nicht mehr auf die Instanz zu – so wie im Beispiel 47.1 und allen folgenden Beispielprogrammen in diesem Kapitel.

Wesentlich interessanter ist die Klasse boost::mpi::communicator. Es handelt sich hierbei um einen Kommunikator, der die Verbindung zu anderen Prozessen herstellt, aus denen das MPI-Programm besteht. Jedem Prozess ist ein Rang zugeordnet. Dabei handelt es sich um eine Ganzzahl – alle Prozesse sind also nummeriert. Ein Prozess kann seinen Rang erfahren, indem er rank() für den Kommunikator aufruft. Möchte ein Prozess wissen, aus wie vielen Prozessen ein MPI-Programm besteht, kann er size() aufrufen.

Wenn Sie Beispiel 47.1 ausführen wollen, müssen Sie dies mit einem Hilfsprogramm machen, das von der von Ihnen verwendeten MPI-Implementation angeboten wird. Bei MPICH heißt dieses Hilfsprogramm mpiexec. Starten Sie Beispiel 47.1 zum Beispiel mit folgendem Befehl:

mpiexec -n 4 beispiel.exe

mpiexec erwartet neben dem Namen des MPI-Programms, das es starten soll, eine Angabe zur Anzahl der Prozesse. So teilen Sie mpiexec zum Beispiel über -n 4 mit, dass vier Prozesse gestartet werden sollen. Ihr Programm wird daraufhin viermal ausgeführt. Dabei handelt es sich jedoch nicht um gänzlich unabhängige Prozesse. Die Prozesse stehen über die MPI-Laufzeitumgebung in Kontakt. Sie gehören alle dem gleichen Kommunikator an. Jedem Prozess ist in diesem Kommunikator ein Rang zugeordnet. Starten Sie Beispiel 47.1 mit vier Prozessen, werden für rank() die Zahlen 0 bis 3 zurückgegeben und für size() 4.

Beachten Sie, dass die Ausgabe des Programms durcheinander sein kann. Es handelt sich schließlich um vier Prozesse, die gleichzeitig auf die Standardausgabe zugreifen. Ob zum Beispiel der Prozess mit Rang 0 oder der mit Rang 3 zuerst Daten auf die Standardausgabe ausgibt, lässt sich nicht vorhersagen. Es ist außerdem möglich, dass ein Prozess einen anderen beim Schreiben auf die Standardausgabe unterbricht und ein Prozess seinen Rang nicht direkt gefolgt von der Größe des Kommunikators ausgeben kann.

Beispiel 47.2. Blockierende Funktionen zum Datenversand und -empfang
#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(1, 16, i);
    std::cout << i << '\n';
  }
  else if (world.rank() == 1)
  {
    world.send(0, 16, 99);
  }
}

boost::mpi::communicator bietet mit send() und recv() zwei einfache Methoden an, um Daten zwischen zwei Prozessen auszutauschen. Es handelt sich hierbei um blockierende Methoden, die erst zurückkehren, wenn Daten gesendet oder empfangen wurden. Das ist vor allem beim Aufruf von recv() wichtig. Wenn Sie recv() aufrufen, ohne dass ein anderer Prozess Daten sendet, kann der Methodenaufruf nicht zurückkehren und der entsprechende Prozess würde im Methodenaufruf verharren.

Beispiel 47.2 ist so strukturiert, dass der Prozess mit Rang 0 mit recv() Daten empfängt, während der Prozess mit Rang 1 mit send() Daten sendet. Starten Sie das Beispiel mit mehr als zwei Prozessen, werden alle anderen Prozesse beendet, ohne dass sie etwas tun.

Sie übergeben drei Parameter an send(): Der erste Parameter ist der Rang des Prozesses, an den Daten gesendet werden sollen. Der zweite Parameter ist ein Tag, der Daten identifiziert. Der dritte Parameter sind die Daten selbst, die gesendet werden sollen.

Der Tag ist immer eine Ganzzahl. Im Beispiel 47.2 ist dies die Zahl 16. Der Tag ermöglicht es, den Aufruf von send() zu identifizieren. So werden Sie gleich sehen, dass der Tag auch beim Aufruf von recv() zum Einsatz kommt.

Als dritten Parameter wird im Beispiel 47.2 die Zahl 99 an send() übergeben. Diese Zahl soll vom Prozess mit dem Rang 1 an den Prozess mit dem Rang 0 gesendet werden. Boost.MPI unterstützt alle primitiven Typen aus C++, so dass Sie einen int-Wert wie 99 problemlos senden können.

recv() erwartet ähnliche Parameter wie send(). Der erste Parameter ist der Rang des Prozesses, von dem Daten empfangen werden sollen. Der zweite Parameter ist der Tag, der den Aufruf von recv() mit dem entsprechenden Aufruf von send() verbindet. Der dritte Parameter ist eine Variable, in der die empfangenen Daten abgelegt werden sollen.

Wenn Sie Beispiel 47.2 mit mindestens zwei Prozessen ausführen, wird 99 auf die Standardausgabe ausgegeben.

Beispiel 47.3. Daten von einem beliebigen Prozess empfangen
#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
  {
    world.send(0, 16, world.rank());
  }
}

Beispiel 47.3 basiert auf dem vorherigen. Jetzt wird nicht mehr die Zahl 99 gesendet, sondern der Rang des Prozesses, der send() aufruft. send() wird außerdem nicht mehr nur vom Prozess mit dem Rang 1 aufgerufen, sondern von allen Prozessen, die einen höheren Rang als 0 haben.

Der Aufruf von recv() ist ebenfalls geändert worden. Als erster Parameter wird boost::mpi::any_source übergeben. Damit wartet recv() nicht mehr auf Daten, die von einem bestimmten Prozess gesendet werden. Egal, welcher Prozess den Tag 16 sendet – die Daten werden vom Prozess mit dem Rang 0 empfangen.

Wenn Sie Beispiel 47.3 mit genau zwei Prozessen ausführen, wird 1 auf die Standardausgabe ausgegeben. Es gibt in diesem Fall nur einen Prozess, der send() aufruft – der Prozess mit dem Rang 1. Starten Sie das Programm mit mehr als zwei Prozessen, lässt sich die Ausgabe nicht vorhersagen. Schließlich rufen in diesem Fall mehrere Prozesse send() auf und versuchen, ihren Rang zu senden. Es hängt vom Zufall ab, welcher Rang auf die Standardausgabe ausgegeben wird.

Beispiel 47.4. Sender mit boost::mpi::status ermitteln
#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;
    boost::mpi::status s = world.recv(boost::mpi::any_source, 16, i);
    std::cout << s.source() << ": " << i << '\n';
  }
  else
  {
    world.send(0, 16, 99);
  }
}

recv() besitzt einen Rückgabewert vom Typ boost::mpi::status. Diese Klasse bietet unter anderem eine Methode source() an, die den Rang des Prozesses zurückgibt, von dem Daten empfangen wurden. Wenn Sie Beispiel 47.4 ausführen, erfahren Sie, von welchem Prozess die Zahl 99 empfangen wurde.

Bisher wurden send() und recv() verwendet, um eine int-Zahl zu übertragen. Im Beispiel 47.5 soll eine Zeichenkette zwischen zwei Prozessen ausgetauscht werden.

Beispiel 47.5. Ein Array mit send() und recv() übertragen
#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)
  {
    char buffer[14];
    world.recv(boost::mpi::any_source, 16, buffer, 13);
    buffer[13] = '\0';
    std::cout << buffer << '\n';
  }
  else
  {
    const char *c = "Hello, world!";
    world.send(0, 16, c, 13);
  }
}

send() und recv() können nicht nur einzelne Werte übertragen, sondern auch Arrays. So wird im Beispiel 47.5 eine Zeichenkette gesendet, die in einem char-Array gespeichert ist. Da send() und recv() primitive Typen wie char unterstützen, kann ein char-Array ohne Probleme übertragen werden.

Beim Aufruf von send() wird als dritter Parameter ein Zeiger auf die Zeichenkette und als vierter Parameter die Länge der Zeichenkette übergeben. recv() erhält als dritten Parameter einen Zeiger auf ein Array, in dem die empfangenen Daten gespeichert werden sollen. Als vierten Parameter wird recv() die Anzahl der Zeichen übergeben, die empfangen und im Array buffer abgelegt werden sollen. Wenn Sie Beispiel 47.5 ausführen, wird Hello, world! auf die Standardausgabe ausgegeben.

Beispiel 47.6. Einen String mit send() und recv() übertragen
#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;
    world.recv(boost::mpi::any_source, 16, s);
    std::cout << s << '\n';
  }
  else
  {
    std::string s = "Hello, world!";
    world.send(0, 16, s);
  }
}

Auch wenn Boost.MPI nur primitive Typen unterstützt, heißt das nicht, dass Sie keine Objekte nicht-primitiver Typen senden und empfangen können. Boost.MPI arbeitet mit Boost.Serialization zusammen. Objekte, die nach den Regeln von Boost.Serialization serialisiert werden können, können von Boost.MPI übertragen werden.

Im Beispiel 47.6 wird Hello, world! übertragen. Diesmal handelt es sich nicht um eine Zeichenkette in einem char-Array, sondern um einen String. Da dieser den Typ std::string hat, kann er nur übertragen werden, wenn er serialisiert werden kann. Boost.Serialization bietet die Headerdatei boost/serialization/string.hpp an, die Sie lediglich einbinden müssen, damit Strings serialisierbar werden.

Wenn Sie Objekte vom Typ benutzerdefinierter Klassen übertragen möchten, sehen Sie sich im Kapitel 64 an, wie Sie im Detail vorgehen müssen.