Die Boost C++ Bibliotheken

Synchronisation

Boost.Interprocess ermöglicht es mehreren Prozessen, einen Shared Memory gemeinsam zu nutzen. Da Prozesse gleichzeitig laufen, konkurrieren sie um den Zugriff auf Daten im Shared Memory. Weil ein Shared Memory eine gemeinsam genutzte Ressource ist, muss Boost.Interprocess die Synchronisation von Zugriffen unterstützen.

Wenn Sie an Synchronisation denken, denken Sie womöglich an Threads. So stehen sowohl in der C++11-Standardbibliothek als auch in Boost.Threads verschiedene Klassen zur Synchronisierung zur Verfügung. Diese Klassen können jedoch ausschließlich zur Synchronisierung von Threads im gleichen Prozess verwendet werden. Sie können sie nicht verwenden, um mehrere Prozesse zu synchronisieren. Da es sich jedoch grundsätzlich um das gleiche Problem handelt – in beiden Fällen geht es um einen synchronisierten Zugriff auf gemeinsam genutzte Ressourcen – werden Ihnen in diesem Abschnitt die gleichen Konzepte wiederbegegnen, die Sie von der C++11-Standardbibliothek oder von Boost.Thread kennen.

Während sich in Multithreaded-Anwendungen, die auf Boost.Thread zugreifen, Synchronisationsobjekte wie Mutexe und Bedingungsvariablen im gleichen Prozess befinden und somit allen Threads zur Verfügung stehen, gibt es bei Shared Memory das Problem, dass sich voneinander unabhängige Prozesse Synchronisationsobjekte teilen müssen. Wenn ein Prozess einen Mutex erstellt, müssen andere Prozesse auf diesen Mutex zugreifen können.

Boost.Interprocess bietet zwei Arten von Synchronisationsobjekten an: Anonyme Synchronisationsobjekte werden direkt im Shared Memory abgelegt, so dass sie dort automatisch allen Prozessen zugänglich sind. Anderen Synchronisationsobjekten werden Namen gegeben, so dass alle Prozesse über diese Namen auf die gleichen Synchronisationsobjekte zugreifen können. Diese Synchronisationsobjekte werden nicht im Shared Memory abgelegt, sondern vom Betriebssystem verwaltet.

Beispiel 33.12. Benannter Mutex mit named_mutex
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")();
  named_mutex named_mtx{open_or_create, "mtx"};
  named_mtx.lock();
  ++(*i);
  std::cout << *i << '\n';
  named_mtx.unlock();
}

Im Beispiel 33.12 sehen Sie, wie Sie einen Mutex mit Namen erstellen und verwenden. Dazu wird auf die Klasse boost::interprocess::named_mutex zugegriffen, die in der Headerdatei boost/interprocess/sync/named_mutex.hpp definiert ist.

Dem Konstruktor der Klasse boost::interprocess::named_mutex müssen Sie neben einem Parameter, der angibt, ob der Mutex geöffnet oder erstellt werden soll, einen Namen übergeben. Jeder Prozess, der den Namen kennt, kann den gleichen Mutex öffnen. Um den Zugriff auf Daten im Shared Memory zu synchronisieren, muss ein Prozess lediglich den Mutex in Besitz nehmen, indem er lock() aufruft. Da ein Mutex immer nur von einem Prozess in Besitz genommen werden kann, muss ein anderer Prozess gegebenenfalls warten, bis der erste Prozess den Mutex mit unlock() freigegeben hat. Wenn ein Prozess einen Mutex in Besitz genommen hat, bedeutet das, dass er exklusiven Zugriff auf eine Ressource hat. Im Beispiel 33.12 ist diese Ressource eine int-Variable, die inkrementiert und auf die Standardausgabe ausgegeben wird.

Wenn Sie Beispiel 33.12 mehrfach starten, wird Ihnen bei jedem Programmstart ein um 1 erhöhter Wert ausgegeben. Auch dann, wenn Sie das Programm mehrfach gleichzeitig starten, ist dank dem Mutex sichergestellt, dass der Zugriff aller gleichzeitig laufender Prozesse auf den gleichen Shared Memory und die gleiche int-Variable synchronisiert stattfindet.

Beispiel 33.13. Anonymer Mutex mit interprocess_mutex
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")();
  interprocess_mutex *mtx =
    managed_shm.find_or_construct<interprocess_mutex>("mtx")();
  mtx->lock();
  ++(*i);
  std::cout << *i << '\n';
  mtx->unlock();
}

Im Beispiel 33.13 wird ein anonymer Mutex vom Typ boost::interprocess::interprocess_mutex verwendet, der im Shared Memory abgelegt werden muss, um allen Prozessen zugänglich zu sein. Er ist in der Headerdatei boost/interprocess/sync/interprocess_mutex.hpp definiert.

Beispiel 33.13 funktioniert genauso wie das vorherige. Der einzige Unterschied ist, dass dieser Mutex im Shared Memory abgelegt ist. Dazu wird die Methode construct() oder find_or_construct() der Klasse boost::interprocess::managed_shared_memory verwendet.

Die Klassen boost::interprocess::named_mutex und boost::interprocess::interprocess_mutex bieten neben der Methode lock() zusätzlich try_lock() und timed_lock() an. Diese Methoden funktionieren genauso wie die gleichnamigen Methoden der Mutex-Klassen aus der C++11-Standardbibliothek und von Boost.Thread.

Für den Fall, dass Sie rekursive Mutexe einsetzen möchten, bietet Boost.Interprocess die beiden Klassen boost::interprocess::named_recursive_mutex und boost::interprocess::interprocess_recursive_mutex an.

Während Mutexe nützlich sind, um exklusiven Zugriff auf gemeinsam genutzte Ressourcen zu erhalten, helfen Bedingungsvariablen zu steuern, wer wann exklusiven Zugriff haben muss. Die von Boost.Interprocess zur Verfügung gestellten Bedingungsvariablen funktionieren grundsätzlich nicht anders als die Bedingungsvariablen in der C++11-Standardbibliothek und in Boost.Thread. Da die Klassen ähnliche Schnittstellen besitzen, sind sie entsprechend einfach zu verwenden.

Beispiel 33.14. Benannte Bedingungsvariable mit named_condition
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/sync/named_condition.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")(0);
  named_mutex named_mtx{open_or_create, "mtx"};
  named_condition named_cnd{open_or_create, "cnd"};
  scoped_lock<named_mutex> lock{named_mtx};
  while (*i < 10)
  {
    if (*i % 2 == 0)
    {
      ++(*i);
      named_cnd.notify_all();
      named_cnd.wait(lock);
    }
    else
    {
      std::cout << *i << std::endl;
      ++(*i);
      named_cnd.notify_all();
      named_cnd.wait(lock);
    }
  }
  named_cnd.notify_all();
  shared_memory_object::remove("shm");
  named_mutex::remove("mtx");
  named_condition::remove("cnd");
}

Im Beispiel 33.14 wird eine Bedingungsvariable vom Typ boost::interprocess::named_condition verwendet. Diese Klasse ist in der Headerdatei boost/interprocess/sync/named_condition.hpp definiert. Da es sich um eine Bedingungsvariable mit Namen handelt, muss sie nicht in einem Shared Memory abgelegt werden.

Das Programm verwendet eine while-Schleife, um eine im Shared Memory abgelegte Variable vom Typ int zu inkrementieren. Während die int-Variable in jedem Schleifendurchgang um 1 erhöht wird, wird sie lediglich in jedem zweiten Schleifendurchgang auf die Standardausgabe ausgegeben: Es werden lediglich ungerade Zahlen ausgegeben.

Jedes Mal, wenn die int-Variable um 1 erhöht wurde, wird auf die Bedingungsvariable named_cnd zugegriffen und wait() aufgerufen. Dieser Methode wird ein Lock übergeben – im Beispiel 33.14 ist dies die Variable lock. Der Lock basiert auf dem RAII-Idiom und nimmt einen Mutex im Konstruktor in Besitz. Im Destruktor wiederum wird der Mutex freigegeben. Der Lock wird vor Beginn der while-Schleife erstellt, womit der Mutex während der Ausführung des gesamten Programms in Besitz genommen ist. Wenn er jedoch als Parameter der Methode wait() übergeben wird, wird er automatisch freigegeben.

Bedingungsvariablen werden verwendet, um zu warten, bis jemand ein Signal schickt, dass die Warterei beendet werden kann. Diese Synchronisation erfolgt über die beiden Methoden wait() und notify_all(). Wenn ein Programm wait() aufruft, gibt es den entsprechende Mutex frei und wartet darauf, dass jemand notify_all() für die gleiche Bedingungsvariable aufruft.

Wenn Sie Beispiel 33.14 starten, stellen Sie fest, dass erst mal nicht viel zu passieren scheint: In der while-Schleife wird die int-Variable von 0 auf 1 erhöht. Anschließend wartet das Programm mit wait() auf ein Signal. Damit dieses Signal irgendwann erfolgt, starten Sie das gleiche Programm ein zweites Mal.

Die zweite Instanz des Programms wird versuchen, den gleichen Mutex in Besitz zu nehmen, bevor die while-Schleife gestartet wird. Das funktioniert, weil die erste Instanz den Mutex durch den Aufruf von wait() freigegeben hat. Die zweite Instanz kann entsprechend die while-Schleife ausführen. Da die erste Instanz die int-Variable im Shared Memory von 0 auf 1 gesetzt hat, wird der else-Zweig der if-Kontrollstruktur ausgeführt. Hier wird der aktuelle Wert der int-Variablen auf die Standardausgabe ausgeben, bevor sie anschließend um 1 erhöht wird.

Auch die zweite Instanz ruft wait() auf. Bevor dies geschieht – und das ist wichtig, damit die beiden Instanzen kooperieren – ruft sie notify_all() auf. Damit wird die erste Instanz informiert, dass der Aufruf von wait() zurückkehren kann. Die erste Instanz weiß, dass sie den Mutex wieder in Besitz nehmen kann, muss sich jedoch einen Augenblick gedulden, da der Mutex momentan von der zweiten Instanz in Besitz genommen ist. Da die zweite Instanz nach notify_all() als Nächstes wait() aufruft und damit den Mutex automatisch freigibt, kann die erste Instanz übernehmen.

So wechseln sich beide Instanzen ab und inkrementieren abwechselnd die int-Variable im Shared Memory. Lediglich eine Instanz aber gibt die Werte auf die Standardausgabe aus. Wenn die int-Variable die Zahl 10 speichert, wird die while-Schleife beendet. Damit die andere Instanz nicht endlos in wait() auf ein Signal wartet, wird nach der while-Schleife noch einmal notify_all() aufgerufen. Abschließend werden der verwendete Shared Memory, der Mutex und die Bedingungsvariable gelöscht.

So wie es zwei Arten von Mutexen gibt – einen anonymen, der im Shared Memory abgelegt werden muss, und einen, der über einen Namen identifiziert wird – gibt es auch zwei Arten von Bedingungsvariablen. Beispiel 33.14 wird nun derart umgeschrieben, dass eine anonyme Bedingungsvariable verwendet wird.

Beispiel 33.15. Anonyme Bedingungsvariable mit interprocess_condition
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <boost/interprocess/sync/interprocess_condition.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")(0);
  interprocess_mutex *mtx =
    managed_shm.find_or_construct<interprocess_mutex>("mtx")();
  interprocess_condition *cnd =
    managed_shm.find_or_construct<interprocess_condition>("cnd")();
  scoped_lock<interprocess_mutex> lock{*mtx};
  while (*i < 10)
  {
    if (*i % 2 == 0)
    {
      ++(*i);
      cnd->notify_all();
      cnd->wait(lock);
    }
    else
    {
      std::cout << *i << std::endl;
      ++(*i);
      cnd->notify_all();
      cnd->wait(lock);
    }
  }
  cnd->notify_all();
  shared_memory_object::remove("shm");
}

Beispiel 33.15 funktioniert genauso wie das vorherige und muss ebenfalls zweimal gestartet werden, damit die int-Variable zehnmal inkrementiert wird.

Neben Mutexen und Bedingungsvariablen, die Sie in diesem Abschnitt kennengelernt haben, unterstützt Boost.Interprocess auch Semaphore und File Locks. Semaphore funktionieren ähnlich wie Bedingungsvariablen, nur dass sie nicht zwischen zwei Zuständen unterscheiden, sondern auf einem Zähler basieren. File Locks wiederum funktionieren wie Mutexe, wobei es sich bei ihnen nicht um Objekte im Speicher handelt, sondern um Dateien im Dateisystem.

So wie die C++11-Standardbibliothek und Boost.Thread verschiedene Typen von Mutexen und Locks unterscheiden, stehen auch in Boost.Interprocess mehrere Mutexe und Locks zur Verfügung. So gibt es neben den in den obigen Beispielen verwendeten Klassen Mutexe, die nicht nur exklusiv in Besitz genommen werden können. Dies ist nützlich, wenn mehrere Prozesse Daten gleichzeitig lesen wollen, da ein Mutex lediglich für Schreibvorgänge exklusiv in Besitz genommen werden muss. Entsprechend stehen verschiedene Klassen für Locks zur Verfügung, mit denen das RAII-Idiom auf die verschiedenen Mutexe angewandt werden kann.

Beachten Sie, dass Sie unbedingt unterschiedliche Namen verwenden sollten, wenn Sie nicht anonyme Synchronisationsobjekte verwenden. Obwohl Mutexe und Bedingungsvariablen auf unterschiedlichen Klassen basieren, gibt es in den von Boost.Interprocess abstrahierten Betriebssystemschnittstellen unter Umständen keine explizite Unterscheidung zwischen diesen Ressourcen. So werden zum Beispiel unter Windows für Mutexe und Bedingungsvariablen die gleichen Betriebssystemfunktionen verwendet. Wenn Sie Mutexen und Bedingungsvariablen den gleichen Namen geben, weil es sich scheinbar um unterschiedliche Objekte handelt, wird Ihr Programm unter Windows nicht wie erwartet funktionieren.

Aufgabe

Entwickeln Sie einen Client und einen Server, die über Shared Memory miteinander kommunizieren. Wenn der Client gestartet wird, soll ihm der Name einer Datei als Kommandozeilenparameter übergeben werden. Die Datei soll über den Shared Memory an den Server gesendet werden. Der Server soll dann die Datei lokal in dem Verzeichnis speichern, in dem er gestartet wurde.