Die Boost C++ Bibliotheken

Verwalteter Shared Memory

Sie haben im vorherigen Abschnitt die Klasse boost::interprocess::shared_memory_object kennengelernt, mit der ein Shared Memory erstellt werden kann. In der Praxis arbeiten Sie eher selten mit dieser Klasse, weil Sie in diesem Fall selbst Bytes im Shared Memory schreiben und lesen müssen. Als C++-Entwickler sind Sie es gewohnt, Objekte vom Typ einer Klasse zu erstellen, ohne sich darum kümmern zu müssen, wie und wo die Bytes dieser Objekte im Speicher abgelegt sind.

Boost.Interprocess bietet ein Konzept an, das sich verwalteter Shared Memory nennt – auf Englisch managed shared memory. Es wird durch die Klasse boost::interprocess::managed_shared_memory zur Verfügung gestellt, die in der Headerdatei boost/interprocess/managed_shared_memory.hpp definiert ist. Mit Hilfe dieses verwalteten Shared Memory wird es möglich, Objekte derart zu instanziieren, dass sich der vom Objekt benötigte Speicher in einem Shared Memory befindet – und das Objekt für andere Prozesse, die auf den gleichen Shared Memory zugreifen, automatisch verfügbar wird.

Beispiel 33.6. Verwalteten Shared Memory verwenden
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  shared_memory_object::remove("Boost");
  managed_shared_memory managed_shm{open_or_create, "Boost", 1024};
  int *i = managed_shm.construct<int>("Integer")(99);
  std::cout << *i << '\n';
  std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer");
  if (p.first)
    std::cout << *p.first << '\n';
}

Im Beispiel 33.6 wird ein verwalteter Shared Memory namens Boost mit einer Größe von 1024 Bytes geöffnet. Wird kein Shared Memory mit diesem Namen gefunden, wird er automatisch erstellt.

Während Sie bei einem Shared Memory, wie Sie ihn im vorherigen Abschnitt kennengelernt haben, direkt auf einzelne Bytes zugreifen, um Daten zu speichern oder zu lesen, rufen Sie für einen verwalteten Shared Memory Methoden wie construct() auf. Diese Methode erwartet als Template-Parameter einen Typ – im Beispiel 33.6 ist int angegeben. Als Funktionsparameter wird ein Name übergeben, mit dem sich das entsprechende Objekt, das im verwalteten Shared Memory erstellt wird, finden lässt. So bekommt im Beispiel 33.6 die int-Variable den Namen Integer.

Da construct() ein Proxy-Objekt zurückgibt, können Sie über eine weitere Klammer Parameter an das Proxy-Objekt übergeben und es auf diese Weise initialisieren. Die Syntax ähnelt einem Konstruktoraufruf. Somit ist sichergestellt, dass Sie mit construct() nicht nur neue Objekte in einem verwalteten Shared Memory erstellen können, sondern sich diese auch wie gewünscht initialisieren lassen.

Um auf ein Objekt im verwalteten Shared Memory zuzugreifen, verwenden Sie die Methode find(). Sie übergeben Ihr den Namen des Objekts, das Sie suchen, und erhalten als Ergebnis einen Zeiger zurück. Wird kein Objekt mit dem angegebenen Namen gefunden, ist der Rückgabewert 0.

Sie sehen im Beispiel 33.6, dass find() den Zeiger in einem Objekt vom Typ std::pair zurückgibt. Der Zeiger wird nicht direkt, sondern über die Eigenschaft first zurückgegeben. Was Sie in second erhalten, lernen Sie anhand des folgenden Beispiels.

Beispiel 33.7. Arrays im verwalteten Shared Memory erstellen
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  shared_memory_object::remove("Boost");
  managed_shared_memory managed_shm{open_or_create, "Boost", 1024};
  int *i = managed_shm.construct<int>("Integer")[10](99);
  std::cout << *i << '\n';
  std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer");
  if (p.first)
  {
    std::cout << *p.first << '\n';
    std::cout << p.second << '\n';
  }
}

Im Beispiel 33.7 wird im Vergleich zum vorherigen nicht eine int-Variable erzeugt, sondern ein Array mit einer Größe von zehn Elementen. Dies geschieht, weil hinter dem Aufruf von construct() in eckigen Klammern die Zahl 10 angegeben ist. Beim Zugriff auf second wird genau diese 10 auf die Standardausgabe ausgegeben. Es ist daher möglich zu erkennen, ob es sich bei einem Objekt, das mit find() gefunden wurde, tatsächlich um ein einzelnes Objekt handelt oder um ein Array. Bei einzelnen Objekten ist second auf 1 gesetzt. Für Arrays ist die entsprechende Größe angegeben.

Beachten Sie, dass im Beispiel 33.7 alle zehn Stellen im Array mit der Zahl 99 initialisiert werden. Möchten Sie die Stellen eines Arrays mit unterschiedlichen Werten initialisieren, müssen Sie als Funktionsparameter einen Iterator übergeben.

Beachten Sie außerdem, dass construct() fehlschlägt, wenn es bereits ein Objekt mit dem angegebenen Namen im verwalteten Shared Memory gibt. In diesem Fall gibt construct() 0 zurück. Wollen Sie das Objekt für den Fall, dass es bereits existiert, wiederverwenden, rufen Sie find_or_construct() auf. Sollte das Objekt bereits existieren, wird nicht 0, sondern ein Zeiger auf das existierende Objekt zurückgegeben. In diesem Fall findet keine Initialisierung statt.

Die Methode construct() kann auch in einem anderen Fall fehlschlagen. Betrachten Sie dazu Beispiel 33.8.

Beispiel 33.8. boost::interprocess::bad_alloc im Fehlerfall
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  try
  {
    shared_memory_object::remove("Boost");
    managed_shared_memory managed_shm{open_or_create, "Boost", 1024};
    int *i = managed_shm.construct<int>("Integer")[4096](99);
  }
  catch (boost::interprocess::bad_alloc &ex)
  {
    std::cerr << ex.what() << '\n';
  }
}

Im Beispiel 33.8 wird versucht, ein Array vom Typ int zu erstellen, das 4096 Stellen umfasst. Der verwaltete Shared Memory ist jedoch lediglich 1024 Bytes groß. Der benötigte Speicher kann vom verwalteten Shared Memory nicht zur Verfügung gestellt werden. Es wird eine Ausnahme vom Typ boost::interprocess::bad_alloc geworfen.

Nachdem Sie gesehen haben, wie Objekte in einem verwalteten Shared Memory erstellt und gefunden werden können, lernen Sie die Methode destroy() kennen, mit der Objekte gelöscht werden können.

Beispiel 33.9. Objekte aus dem verwalteten Shared Memory entfernen
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  shared_memory_object::remove("Boost");
  managed_shared_memory managed_shm{open_or_create, "Boost", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")(99);
  std::cout << *i << '\n';
  managed_shm.destroy<int>("Integer");
  std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer");
  std::cout << p.first << '\n';
}

Wie im Beispiel 33.9 zu sehen, übergeben Sie der Methode destroy() den Namen des Objekts, das Sie löschen möchten. Diese Methode gibt ein Ergebnis vom Typ bool zurück, falls Sie wissen möchten, ob ein Objekt mit dem angegebenen Namen gefunden und gelöscht wurde. Ein Objekt wird immer gelöscht, wenn es gefunden werden konnte. Gibt die Methode false zurück, bedeutet dies, dass kein Objekt mit dem angegebenen Namen gefunden werden konnte.

Neben destroy() steht eine Methode destroy_ptr() zur Verfügung, der Sie einen Zeiger auf ein Objekt im verwalteten Shared Memory übergeben können. Sie können destroy_ptr() auch für Arrays verwenden.

Nachdem Sie gesehen haben, wie einfach es mit verwaltetem Shared Memory ist, Objekte in einem mit anderen Prozessen gemeinsam genutzten Speicherbereich zu legen, möchten Sie womöglich auch Container aus der Standardbibliothek verwenden. Da Container dynamisch Speicher reservieren, muss ihnen mitgeteilt werden, dass dieser Speicher in einem verwalteten Shared Memory reserviert werden soll und nicht wie üblich mit new da, wo es das Betriebssystem für sinnvoll hält.

Viele Implementationen der Standardbibliothek sind nicht flexibel genug, um die von ihnen angebotenen Container wie std::string oder std::list mit Boost.Interprocess verwenden zu können. Das schließt die Implementationen der Standardbibliothek ein, die mit Visual C++ 2013, GCC und Clang ausgeliefert werden.

Um Entwicklern dennoch die Möglichkeit zu bieten, die aus dem Standard bekannten Container verwenden zu können, bietet Boost.Interprocess im Namensraum boost::interprocess flexible Implementationen der gleichen Klassen an. So steht zum Beispiel eine Klasse boost::interprocess::string zur Verfügung, die genauso funktioniert wie std::string. Der Vorteil ist, dass Objekte vom Typ boost::interprocess::string auf alle Fälle in einem verwalteten Shared Memory abgelegt werden können, selbst wenn das mit Objekten vom Typ std::string wie bei vielen heutigen Implementationen der Standardbibliothek nicht funktioniert.

Beispiel 33.10. Strings im verwalteten Shared Memory ablegen
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  shared_memory_object::remove("Boost");
  managed_shared_memory managed_shm{open_or_create, "Boost", 1024};
  typedef allocator<char,
    managed_shared_memory::segment_manager> CharAllocator;
  typedef basic_string<char, std::char_traits<char>, CharAllocator> string;
  string *s = managed_shm.find_or_construct<string>("String")("Hello!",
    managed_shm.get_segment_manager());
  s->insert(5, ", world");
  std::cout << *s << '\n';
}

Um wie im Beispiel 33.10 einen String im verwalteten Shared Memory erstellen zu können, der bei einem Aufruf von insert() den zusätzlich benötigten Speicher automatisch im gleichen verwalteten Shared Memory reserviert, muss ein entsprechender Typ definiert werden. Denn der Typ, auf dem der String basiert, darf nicht den Standardallokator aus dem Standard verwenden, sondern muss auf einen Allokator von Boost.Interprocess zugreifen.

Boost.Interprocess bietet eine Klasse boost::interprocess::allocator an, die in der Headerdatei boost/interprocess/allocators/allocator.hpp definiert ist. Mit dieser Klasse kann ein Allokator erstellt werden, der den Segment-Manager des verwalteten Shared Memory verwendet. Der Segment-Manager ist für die Verwaltung des Speichers in einem verwalteten Shared Memory verantwortlich. Mit dem neu definierten Allokator kann ein entsprechender Typ für den String definiert werden, wobei wie bereits erwähnt auf boost::interprocess::basic_string und nicht auf std::basic_string zugegriffen wird. Der neu definierte Typ – im Beispiel 33.10 string genannt – basiert auf der Implementation von boost::interprocess::basic_string und greift über den Allokator auf einen Segment-Manager zu. Damit die Instanz von string, die durch den Aufruf von find_or_construct() erstellt wird, weiß, auf welchen Segment-Manager sie zuzugreifen hat, wird der Zeiger auf den Segment-Manager als zweiter Parameter an den Konstruktor übergeben.

Neben boost::interprocess::string bietet Boost.Interprocess für viele andere aus der Standardbibliothek bekannte Container eigene Implementationen an. So stehen zum Beispiel Container wie boost::interprocess::vector und boost::interprocess::map zur Verfügung, die in den entsprechenden Headerdateien boost/interprocess/containers/vector.hpp und boost/interprocess/containers/map.hpp definiert sind.

Beachten Sie, dass die Container aus Boost.Container Boost.Interprocess unterstützen und in verwaltetem Shared Memory abgelegt werden können. Sie müssen nicht zwangsläufig auf die Container in boost::interprocess zugreifen. Boost.Container wird im Kapitel 20 vorgestellt.

Wenn Sie in mehreren Prozessen auf den gleichen verwalteten Shared Memory zugreifen und Objekte erstellen, suchen und zerstören, werden diese Operationen automatisch synchronisiert ausgeführt. Wenn zwei Prozesse zur gleichen Zeit versuchen, zwei Objekte mit unterschiedlichem Namen in einem verwalteten Shared Memory zu erstellen, wird der Zugriff auf den Speicher derart synchronisiert, dass beide Funktionen nacheinander ausgeführt werden. Möchten Sie mehrere Operationen gemeinsam ausführen, ohne dass diese eventuell von Operationen in einem gleichzeitig ausgeführten Prozess unterbrochen werden, bietet sich die Methode atomic_func() an. Sehen Sie sich dazu Beispiel 33.11 an.

Beispiel 33.11. Atomarer Zugriff auf verwalteten Shared Memory
#include <boost/interprocess/managed_shared_memory.hpp>
#include <functional>
#include <iostream>

using namespace boost::interprocess;

void construct_objects(managed_shared_memory &managed_shm)
{
  managed_shm.construct<int>("Integer")(99);
  managed_shm.construct<float>("Float")(3.14);
}

int main()
{
  shared_memory_object::remove("Boost");
  managed_shared_memory managed_shm{open_or_create, "Boost", 1024};
  auto atomic_construct = std::bind(construct_objects,
    std::ref(managed_shm));
  managed_shm.atomic_func(atomic_construct);
  std::cout << *managed_shm.find<int>("Integer").first << '\n';
  std::cout << *managed_shm.find<float>("Float").first << '\n';
}

Sie übergeben der Methode atomic_func() als einzigen Parameter eine Funktion, die keinen Rückgabewert besitzt und keinen Parameter erwartet. Diese Funktion wird derart aufgerufen, dass Sie in ihr exklusiven Zugriff auf den verwalteten Shared Memory haben – jedenfalls, was die Methoden zum Erstellen, Suchen und Löschen von Objekten betrifft. Hat zum Beispiel ein anderer Prozess bereits einen Zeiger auf ein Objekt im verwalteten Shared Memory erhalten, kann er über diesen Zeiger auf das Objekt zugreifen und es verändern.

Sie können mit Boost.Interprocess auch den Zugriff auf Objekte synchronisieren. Da Boost.Interprocess nicht wissen kann, wer wann auf die von Ihnen verwendeten Objekte wie zugreifen darf, müssen Sie die Zugriffe selbst synchronisieren. Die Klassen, die Boost.Interprocess zur Synchronisation zur Verfügung stellt, lernen Sie im nächsten Abschnitt kennen.