Parallelprogrammierung mit C++

Schneller zum Ziel
Kommentare

Mit der steigenden Anzahl von Multi-Core-CPUs in modernen Rechnern steigt gleichzeitig der Bedarf nach mit Mitteln der Parallelprogrammierung erstellter Software, die diese verbesserten Hardwareressourcen auch ausnutzen kann. Vorgestellt werden in diesem Artikel Techniken, mit denen C++-Entwickler ihre Anwendungen parallelisieren können.

Einer der wichtigsten Trends auf dem aktuellen Hardwaremarkt ist die immer weitergehende Verbesserung der modernen Rechner durch Multi-Core-CPUs. Diese Entwicklung betrifft bei Weitem nicht nur hochpreisige Server für Rechenzentren, sondern vermehrt auch den Markt für (private) Desktop- und Notebook-Rechner. Wer sich aber alleine durch verbesserte Hardware eine Performancesteigerung seiner bestehenden Anwendungen nach dem Grundsatz „n-mal mehr Prozessorkerne ergibt eine n-mal schnellere Verarbeitung“ erhofft, der wird in den meisten Fällen zunächst schwer enttäuscht sein. So einfach lässt sich die Performance ausschließlich durch Hardwareinvestitionen nicht steigern. Auch auf der Softwareseite sind hierfür Veränderungen erforderlich. Der Grund liegt meist darin, dass viele (Alt-)Anwendungen nur für Ein-Core-CPUs entwickelt wurden und somit die verbesserten Hardwareressourcen nicht oder nur unvollständig ausnutzen können.

Im Zuge der Hardwareverbesserungen entsteht also auch an die Softwareentwicklung die Anforderung, dass die zu erstellenden Systeme durch parallele Abarbeitung von definierten Blöcken auf mehreren Prozessor-Kernen schneller zu den gewünschten Ergebnissen kommen. Wer in seiner Ausbildung Vorlesungen über Parallelprogrammierung gehört hat und sich bisher nicht um diese Themen kümmern brauchte oder durfte, der steht nun also vor der Aufgabe, diese Konzepte in der für das Projekt ausgewählten Programmiersprache zu realisieren. In der Regel ist man dabei nicht in der glücklichen Lage, die Sprache ausschließlich anhand ihrer Eignung für Parallelprogrammierung auszuwählen (und sich zu diesem Zweck beispielsweise Clojure oder Scala anzusehen), da die Projekt-Programmiersprache meist schon aus anderen Gründen festgelegt wurde.

In diesem Artikel soll es nun um Konzepte und Tools für die Parallelprogrammierung mit C++ gehen. Im vorhergehenden Artikel dieser C++-Reihe in der letzten Ausgabe wurde die Sprache C++ bereits mit ihrer Historie sowie ihren wichtigsten Bestandteilen vorgestellt. Nach einer Einführung in die Theorie der Parallelprogrammierung wird zunächst erläutert, welche Bestandteile der aktuelle Standard C++11 für die Entwicklung von parallelen Anwendungen zur Verfügung stellt. Neben diesen C++-Standardkonzepten gibt es aber auch eine Vielzahl von externen Bibliotheken, die teilweise bereits seit vielen Jahren existieren und daher auch schon in diversen Projekten ihre Praxistauglichkeit bewiesen haben. Einige dieser externen Lösungen werden ebenfalls in diesem Artikel bzw. im Folgeartikel über C++-Bibliotheken im kommenden Heft vorgestellt.

Theorie der Parallelprogrammierung

Was sind nun die Grundkonzepte der parallelen Programmierung? Anhand des (Pseudo-)Beispielcodes aus Listing 1 werden diese im Folgenden kurz vorgestellt. Der Code in Listing 1 ähnelt zwar der Thread-Programmierung mit Boost (folgender Artikel über C++-Bibliotheken im nächsten Heft), ist aber bewusst auf die grundlegenden Konzepte zusammengeschnitten worden.

Während man in der „normalen Programmierung“ davon ausgehen kann, dass die implementierten Schritte des Quellcodes zur Laufzeit sukzessive verarbeitet werden, teilt man bei der Parallelprogrammierung dem Laufzeitsystem durch die Erstellung so genannter Threads mit, dass nach dem Start eines solchen Threads, den man sich vereinfacht auch als ein Unterprogramm vorstellen kann, zunächst nicht auf dessen komplette Verarbeitung gewartet werden muss, sondern direkt mit der nächsten Anweisung fortgefahren werden kann. An dieser Stelle kommt nun die Eignung solcher Multi-Threaded-Programme für Multi-Core-Prozessoren ins Spiel. Jeder Thread kann nun auf einem eigenen Core ablaufen und wenn mehr als ein solcher Core zur Verfügung steht, dann ermöglicht man also die parallele Verarbeitung mehrerer Threads.

#include <xyz/thread.hpp>
void aufgabe1() {
  // Arbeitsschritte für Aufgabe 1
}
void aufgabe2() {
  // Arbeitsschritte für Aufgabe 2
}

main() {
  thread thread_1 = thread(aufgabe1);
  thread thread_2 = thread(aufgabe2);

  // weitere Arbeitsschritte
  // …
  thread_1.join();
  thread_2.join();

  // Abschlussoperationen
}

Bei der Erstellung eines Threads gibt man in der Regel eine Funktion an, die die Aufgabe des Threads beschreibt. Im Beispiel aus Listing 1 werden also zwei Threads angelegt und vom Laufzeitsystem gestartet: der erste Thread thread_1 führt die Anweisungen in der Funktion aufgabe1() aus, während thread_2 mit der Abarbeitung von aufgabe2() beschäftigt ist. Zusätzlich zu den gestarteten Threads können nun innerhalb des Programm-Haupt-Threads (s. main()-Methode) noch weitere Anweisungen ausgewertet werden, für die die Ergebnisse der parallel laufenden Threads noch nicht benötigt werden. Erst wenn über eine Anweisung der Art thread_1.join(); explizit auf die Beendigung eines Threads gewartet wird, wird die weitere Bearbeitung des Haupt-Threads angehalten, bis der angegebene Thread komplett verarbeitet ist.

Wichtig für das weitere Verständnis sowie die im Folgenden noch beschriebenen Probleme beim Debuggen solcher Anwendungen ist die Tatsache, dass die exakte Reihenfolge, in der die Anweisungen aus aufgabe1() und aufgabe2() ausgeführt werden, nicht steuerbar ist und somit von Lauf zu Lauf unterschiedlich sein kann. Während die Reihenfolge der Anweisungen innerhalb von aufgabe1() und innerhalb von aufgabe2() der implementierten Reihenfolge entspricht, können also folgende Situationen von Lauf zu Lauf verschieden auftreten:

  • Zuerst alle Anweisungen von aufgabe1(), danach alle Anweisungen von aufgabe2()
  • Jeweils eine Anweisung von aufgabe1() und aufgabe2() im Wechsel
  • Erst die erste Hälfte von aufgabe1(), dann die komplette aufgabe2() und abschließend die zweite Hälfte von aufgabe1()

Solange aufgabe1() und aufgabe2() völlig unabhängig sind, also insbesondere nicht auf gemeinsame Speicherbereiche oder Betriebssystemressourcen zugreifen, kann man diese Variationen in der Reihenfolge vernachlässigen, sofern für die weitere Verarbeitung jeweils nur das Endergebnis der beiden Funktionen und somit der beiden Threads relevant ist. Spätestens wenn auf gemeinsame Ressourcen (schreibend) zugegriffen wird, muss der Entwickler aber für eine Synchronisation dieser Zugriffe sorgen, indem er den parallelen Zugriff auf die Ressource verhindert.

Die Erweiterung des ersten Beispiels in Listing 2 zeigt eine solche Synchronisierung mittels eines Mutex. Diesen kann man sich als eine Art Sperre vorstellen. Jeder Thread, der bei seiner Verarbeitung eine Stelle der Art mutex.lock() erreicht, muss beim Laufzeitsystem zunächst das Setzen dieser Sperre beantragen. Das Laufzeitsystem wiederum garantiert nun, dass immer nur maximal ein Thread eine solche Sperre setzen darf. Ist die Sperre gesetzt, so kann der betreffende Thread mit seiner Verarbeitung fortfahren. Alle weiteren Threads, die nun ebenfalls diese Sperre setzen wollen, werden zunächst angehalten. Erst wenn der erste Thread mittels mutex.unlock() die Sperre wieder aufhebt, bekommt der nächste auf die Sperre wartende Thread diese zugewiesen und kann nun ebenfalls den durch den Mutex geschützten Bereich seiner Anwendung ausführen. Im Beispiel wird der Zugriff auf eine globale zaehler-Variable geschützt. Somit ist sichergestellt, dass der in einem Thread erhöhte Wert von zaehler während der gesamten vom Mutex geschützten Befehlssequenz nicht durch einen anderen Thread verändert werden darf, was ansonsten Ursache für „unerwartete Effekte“ sein könnte.

#include <xyz/thread.hpp>

xyz::mutex mutex; 
int zaehler;

void aufgabe1() {
  // Arbeitsschritte für Aufgabe 1 (erster Teil) 

  mutex.lock();
  zaehler++;
  // von zaehler-Wert abhängige Arbeitsschritte
  mutex.unlock(); 

  // Arbeitsschritte für Aufgabe 1 (letzter Teil) 
}

void aufgabe2() {
  // Arbeitsschritte für Aufgabe 2 (erster Teil) 

  mutex.lock();
  zaehler++;
  // von zaehler-Wert abhängige Arbeitsschritte
  mutex.unlock(); 

  // Arbeitsschritte für Aufgabe 2 (letzter Teil) 
}

main() {
  thread thread_1 = thread(aufgabe1);
  thread thread_2 = thread(aufgabe2);

  // weitere Arbeitsschritte
  // …

  thread_1.join();
  thread_2.join();

  // Abschlussoperationen

}

Nicht fehlen darf an dieser Stelle natürlich der Hinweis auf das Problem der fehlenden Mutex-Freigabe, sofern innerhalb des durch den Mutex geschützten Bereiches eine Exception geworfen und die Zeile der Mutex-Freigabe daher nicht durchlaufen wird. Es droht in solchen Fällen eine Blockade des gesamten Programms, da andere Threads möglicherweise noch auf die Freigabe des Mutex warten und somit in eine Endlos-Warteschleife geraten können. Um dies zu verhindern wird – nicht nur bei der Arbeit mit Mutex-Variablen – die Verwendung von Smart Pointern std::shared_ptr() in C++-Anwendungen empfohlen, die sicherstellen, dass alle Ressourcen eines Blockes an dessen Ende freigegeben werden – unabhängig davon, wie die Verarbeitung des Blockes endet.

Worin liegt nun die Komplexität bei der Parallelprogrammierung? Während man sich noch vergleichsweise schnell in die grundlegenden Konzepte für die Entwicklung von parallelen Algorithmen einarbeiten kann und durch diverse Tools auch gute Unterstützung bei der Realisierung erster paralleler Anwendungen erhält, so erfährt man spätestens bei der Fehlersuche in parallelen Programmen, warum dies von vielen als eines der schwierigsten und manchmal auch zeit- und nervenaufreibendsten Tätigkeiten eines Softwareentwicklers bezeichnet wird. Man gibt zwar durch seinen Sourcecode die Regeln für die Parallelverarbeitung vor, aber die eigentliche Reihenfolge, in der die (atomaren) Operationen zur Laufzeit ausgeführt werden, bestimmt eine spezielle Komponente des Betriebssystems. Es kann also durchaus vorkommen, dass ein Fehler nur in einer bestimmten, vom Entwickler nicht selber festzulegenden Reihenfolge von Einzelschritten auftritt. Es liegt also eine in der Parallelprogrammierungs-Theorie als Race Condition beschriebene Situation vor, in der das Ergebnis einer parallel durchgeführten Berechnung von der Reihenfolge der durchgeführten Einzelschritte abhängig ist. Für Tester und Entwickler auf Bug-Suche kann dies zum Worst Case werden, denn möglicherweise ist eine Software durch eine Vielzahl von Testfällen ausgiebig in der Entwicklungs- und Testumgebung auf Herz und Nieren getestet worden und läuft dann trotzdem in der Produktionsumgebung erstmalig und nicht reproduzierbar in eine Fehlersituation. Führt man sich vor Augen, dass C++-Anwendungen häufig in unternehmenskritischen Serveranwendungen laufen, bei denen oft auch sicherheitsrelevante Anlagen gesteuert werden, so erkennt man schnell, welche Bedeutung eine intensive Einarbeitung in das Thema sowie eine saubere Programmierung bei der Erstellung von parallelen C++-Anwendungen haben. Auch kleine Änderungen am Sourcecode bei Release-Wechseln können zu Situationen führen, die erstmalig zu „seltsamen Nebeneffekten führen“ und mithilfe von Debuggern entweder überhaupt nicht oder nur mit sehr hohem Aufwand nachzustellen sind.

Ein weiterer Begriff, den man im Zusammenhang mit Fehlermeldungen bei parallelen System häufig hört, ist der des „Deadlocks“. Wird eine Sperre nicht ohne weitere Bedingungen wie in Listing 2 angefragt sondern in Abhängigkeit einer bestimmten Konstellation, so sind Situationen denkbar, in denen Thread A auf eine Konstellation wartet, die im geschützten Bereich von Thread B erzeugt werden kann und Thread B gleichzeitig auf eine von Thread A herbeizuführende Situation (z. B. die Freigabe einer Betriebssystemressource) wartet. Auch wenn es innerhalb eines Betriebssystems spezielle Algorithmen gibt, mit denen zur Laufzeit solche Konstellationen möglichst frühzeitig erkannt und dann verhindert werden sollen, so entbindet dies den Entwickler nicht von seiner Verantwortung, solche Situationen bereits bei der Implementierung zu verhindern.

Was bietet der C++-Standard

Das Thema Parallelprogrammierung gewinnt zwar insbesondere in den letzten Jahren durch die beschriebenen Hardwareverbesserungen immer mehr an Bedeutung, existiert im C++-Bereich aber bereits seit vielen Jahren. Dementsprechend gibt es bereits eine Vielzahl von Bibliotheken von Drittanbietern, mit denen die Programmierung von Threads z. B. nach dem Muster von Listing 1 und 2 möglich ist. Einige dieser Lösungen werden im Folgenden noch einzeln vorgestellt. Etwas hinter den Möglichkeiten der externen Bibliotheken blieb bis vor Kurzem der offizielle C++-Standard zurück. Das ändert sich aber nun mit der im letzten Jahr verabschiedeten Version C++11, bei der Threads in den Standard integriert wurden.

Die technische Realisierung dieser „C++-Standard-Threads“ orientiert sich – wie bei einigen anderen Standardergänzungen auch – an der Boost-Bibliothek, die in einem Folgeartikel in der nächsten Ausgabe noch näher vorgestellt wird und die damit wieder ihre Rolle als wichtige Stelle für C++-Spracherweiterungen bewiesen hat, die zu einem späteren Zeitpunkt in den Standard übernommen werden.

Eine standardkonforme Thread-Implementierung unterscheidet sich daher nur wenig von dem in Listing 1 und 2 dargestellten Pseudo-Sourcecode, der sich an der Boost-Implementierung orientiert. Zentrale Komponente ist die neue thread-Klasse aus der C++-Standard-Library std::thread. Was sich mit C++11 ändert ist die Tatsache, dass das für eine Thread-Anwendung benötigte interne Speichermodell nun ebenfalls in den Standard übernommen wurde, d. h. jeder Compiler, der C++11-konform arbeitet, erzeugt nun – ohne Einbeziehung externer Bibliotheken – Speicherstrukturen, mit denen eine höhere Portabilität der parallelen Bestandteile erreicht wird. Die Abhängigkeit von einer zu Projektbeginn gewählten externen (Thread-)Bibliothek, die insbesondere bei einer späteren Portierung größerer Softwaresysteme zu aufwändigen Migrationsprojekten führen kann, soll also bei standardkonformem Vorgehen deutlich reduziert werden.

OpenMP

Mit der OpenMP-Spezifikation existiert bereits seit dem Jahr 1996 ein herstellerübergreifender Ansatz, bei dem die zu parallelisierenden Teile einer Anwendung durch pragma-Anweisungen gekennzeichnet werden. Eine solche pragma-Anweisung ist ein Compiler-Direktiv, über den der Compiler angewiesen wird, das im auf die pragma-Anweisung folgenden Block befindliche Codefragment über mehrere Threads parallel abzuarbeiten. Syntaktisch sieht eine solche pragma-Anweisung immer wie folgt aus:

#pragma omp <Anweisung>

Nach dem Schlüsselwort omp folgt die eigentliche Anweisung gemäß der OpenMP-Spezifikation. Im folgenden Beispiel wird durch die Anweisung parallel eine Reihe von Threads erzeugt, in denen jeweils die im folgenden Codeblock implementierten Anweisungen parallel ausgeführt werden:

#pragma omp parallel
{
  // Folge von C++-Anweisungen
}

Die Anzahl der Threads kann entweder vom Compiler festgelegt werden und entspricht dabei meist der Anzahl der zur Verfügung stehenden Prozessor-Kerne – oder auch über die spezielle OpenMP-Syntax num_threads vom Entwickler angegeben werden:

#pragma omp parallel num_threads(v_anzahl_threads)

Auch eine for-Schleife kann mit OpenMP-Mitteln parallelisiert werden:

#pragma omp parallel
{
  #pragma omp for
  for(int i=0; i<100; ++i)
    // do something
}

Hiermit werden die einzelnen Durchläufe der for-Schleife nicht sequenziell, sondern nebenläufig in mehreren Threads ausgeführt. Zu beachten ist dabei, dass die Reihenfolge der Durchläufe nicht definiert ist. Das heißt man darf nicht davon ausgehen, dass sie der Reihenfolge einer sequenziellen Verarbeitung entspricht und sollte daher keine entsprechenden Abhängigkeiten in die Durchläufe einbauen.

Will man den Zugriff auf gemeinsame Ressourcen schützen und immer nur maximal einem Thread einen Zugriff darauf erlauben, so erfolgt dies bei OpenMP durch die Definition eines „critical block“. Dieser wird mit der Syntax

#pragma omp critical
{
  // Zugriff auf gemeinsame Ressourcen
}

gekennzeichnet und gewährleistet so, dass Threads, die diesen Block erreichen, angehalten werden, sofern sich zu diesem Zeitpunkt bereits ein anderer Thread in demselben Block befindet. Erst wenn dieser Thread den Block wieder verlassen hat, wird dem nächsten wartenden Thread der Eintritt in den Block gewährt.

Die OpenMP-Spezifkation enthält noch eine Vielzahl weiterer Anweisungen. OpenMP wird neben der C++-Welt auch noch für die Programmiersprachen C und Fortran angeboten. Unterstützt wird OpenMP von mehreren C++-Compilern, u. a. auch vom gcc-Compiler mit der Compile-Option –fopenmp.

Software Transactional Memory

Einen anderen Ansatz als die bisher beschriebenen Verfahren verfolgt das Konzept des Software Transactional Memory (STM), um Teile einer Anwendung parallel ausführen zu können. Es orientiert sich dabei am Transaktionskonzept aus der Datenbankwelt, das auf die (C++-)Programmierung übertragen werden soll. STM ist keine Lösung ausschließlich für C++, sondern existiert auch für andere Sprachen. Zu nennen ist an dieser Stelle insbesondere die Verwendung in der Sprache Clojure.

Wer mit (relationalen) Datenbanken arbeitet, wird den Begriff Transaktion kennen. Er kennzeichnet eine Folge von Anweisungen an die Datenbank, die entweder vollständig (commit) oder gar nicht (rollback) dauerhaft in den Datenbankspeicher geschrieben werden sollen. Aufgabe der Datenbank ist die Sicherstellung bestimmter Eigenschaften (ACID-Prinzip) des Datenbestandes – insbesondere dann, wenn mehrere Transaktionen aus parallel laufenden Datenbank-Sessions lesend und/oder schreibend auf identische Datenbankinhalte zugreifen.

Bereits an dieser Beschreibung erkennt man Ähnlichkeiten zu Problemstellungen in der Parallelprogrammierung. Für den Entwickler als Nutzer von STM-Tools vereinfacht sich die Parallelisierung seiner Algorithmen, da er zunächst nur die Bereiche kennzeichnen muss, die als parallele Tasks ablaufen sollen. Aus der derzeit schon verfügbaren Dokumentation des gcc-Compilers zu STM ist dazu folgendes Beispiel entnommen:

__transaction_atomic { c = a - b; }

Wie bei Datenbankzugriffen, die durch eine vergleichbare Syntax wie

BEGIN TRANSACTION
END TRANSACTION

gekennzeichnet sind, überlässt der Entwickler die konkrete Realisierung der Parallelität, also insbesondere einen möglichen parallelen Zugriff auf Variablen aus anderen durch __transaction_atomic{} gekennzeichneten Bereichen, der Laufzeitumgebung. Diese wird zunächst alle parallelen Operationen auf eigenen Kopien der betroffenen Speicherbereichen ausführen. Sobald ein „Thread“ beendet ist, wird – in Anlehnung an ein Datenbank-Commit – überprüft, ob sich die im lokalen Datenbestand des Threads durchgeführten Änderungen mit den Änderungen aus anderen Teilen „vertragen“. Ist dies nicht der Fall, so wird die Transaktion komplett zurückgerollt und neu gestartet. An dieser Stelle erkennt man eine wichtige Voraussetzung für den Einsatz von STM, die gleichzeitig zum Ausschluss dieser Technologie führen kann. Es muss nämlich vom Entwickler sichergestellt werden, dass alle Operationen, die in dem durch __transaction_atomic{} gekennzeichneten Block stehen, keine nicht zurücksetzbaren Nebeneffekte (z. B. Ausgaben auf externe Ressourcen) haben.

Die Grundidee, dass zunächst alle als zu parallelisieren gekennzeichneten Bereiche tatsächlich parallel auf einer eigenen Kopie des Datenbestandes arbeiten, lässt sich mit dem Begriff des „Optimistischen Locking“ aus der Datenbankwelt vergleichen und führt zunächst einmal zu Performancevorteilen, insbesondere bei ausschließlich lesenden Operationen. Aber auch der Nachteil des erhöhten Speicherbedarfes durch die eigenen Kopien für jeden Thread liegt auf der Hand.

STM ist vor allem wegen seiner für den Entwickler einfacheren Handhabung eine interessante Option. Ein Großteil der Komplexität verschiebt sich in den Verantwortungsbereich der Compiler-Anbieter. Version 4.7 des gcc-Compilers bietet eine erste Unterstützung. Allerdings ist das ganze derzeit noch Bestandteil von Forschungsarbeiten und daher bei Weitem noch keine komplett ausgereifte Technologie, oder wie es auf den Webseiten zum gcc-Compiler heißt: „The support is experimental“. Ob man dies schon für produktive Projekte einsetzen will, muss jeder für sich entscheiden.

Weitere Bibliotheken

Es existiert eine Vielzahl von Bibliotheken, die den Entwickler bei der Parallelprogrammierung in C++ unterstützen. Nicht alle konnten in diesem Artikel näher vorgestellt werden. Erwähnt werden sollen aber zumindest noch die Parallel Patterns Library von Microsoft, die in Zusammenhang mit Visual Studio 2010 vorgestellt wurde, sowie die Intel Threading Building Blocks-Bibliothek, die neben einer GPLv2-lizensierten Open-Source-Version auch mit kommerziellem Support von Intel angeboten wird.

Zwei der bekanntesten Thread-Bibliotheken sind die beiden aus den Frameworks Boost und Qt. Diese wurden in diesem Artikel bewusst noch nicht näher vorgestellt, da sich der Folgeartikel in der nächsten Ausgabe ausgiebig mit diesen beiden Frameworks beschäftigen wird.

Fazit

Mit einem lachenden und einem weinenden Auge wird mancher C++-Entwickler auf die Integration der Thread-Lösungen in den C++-Standard C++11 blicken. Einerseits erscheint es aus den beschriebenen Portabilitätsgründen attraktiv, bei einer so wichtigen Komponente wie der Threading-Lösung auf ein standardisiertes Vorgehen zu setzen und sich damit nicht in die Abhängigkeit eines Drittanbieters zu begeben. Andererseits kommt diese Standardintegration für viele existierende Projekte einige Jahre zu spät. Wer bereits eine Threading-Lösung mit einer externen Bibliothek im Einsatz hat, wird es sich sehr gut überlegen, ob er beim nächsten Release seiner Software tatsächlich das Risiko einer Umstellung an so einer wichtigen Stelle vornehmen will, nur um standardkonform zu werden.

Die vorgestellten externen Lösungen werden also sicherlich auch in Zukunft nicht an Bedeutung verlieren. Vor manchen Marketing-Versprechen der Anbieter solcher Lösungen muss aber ganz klar gewarnt werden. Aussagen der Art „Mit unserer Threading-Lösung werden Sie parallele Programme erstellen können, ohne sich in die Tiefen der Parallelprogrammierung einarbeiten zu müssen“ sind sehr gefährlich und gelten sicher nur für Programme auf „Hello World“-Niveau. Bei der Erstellung bzw. spätestens bei der Fehlersuche in komplexeren (C++-)Threading-Anwendungen ist viel Erfahrung erforderlich, um auch „unerklärliche Phänomene“ zu verstehen bzw. idealerweise erst gar nicht entstehen zu lassen.

 

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -