Open Source im professionellen Einsatz
Linux-Magazin 06/2014

Modernes C++ in der Praxis – Folge 16

Der Vertrag

Der C++-Spezialist Rainer Grimm nimmt ein diffiziles Thema in Angriff: Das neue Memory-Modell von C++11 führt unter Umständen zu überraschendem Verhalten von Programmen. Für mehr Disziplin sorgen atomare Variablen und die so genannte Memory-Ordnung.

577

Das neue Memory-Modell von C++ beschreibt eine abstrakte Maschine. Es sichert dem Entwickler einerseits definiertes Programmverhalten zu, lässt dem System aber andererseits die Freiheit, eine angepasste ausführbare Datei zu erzeugen. Dieser Vertrag zwischen Programmierer und System ist die Grundlage für hochoptimierten ausführbaren Code, der für die gewünschte Plattform maßgeschneidert ist. Denn was zählt, ist – wie immer – die Performance.

Das System besteht aus dem Compiler, den Prozessoren und den verschiedenen Speichern. Jede der drei Komponenten versucht den resultierenden Code zu optimieren. So kann der Compiler Schleifendurchläufe vereinfachen, unbenutzte Variablen entfernen, Maschinenbefehle eliminieren oder Ausdrücke vorab berechnen. Die Prozessoren dürfen Anweisungen ignorieren oder umordnen, die verschiedenen Speicherlevels (Caches) können Werte zwischenspeichern oder verzögert den anderen Speichern zur Verfügung stellen. Diese Optimierungen gehen von der Annahme aus, dass ein einziger Kontrollfluss existiert. Kein Wunder, dass dieses Modell in Multithreading-Umgebungen eindeutige Regeln in Form eines formalen Vertrags benötigt.

Memory-Modell

Mit C++11 erhielt C++ in Anlehnung an das Memory-Modell von Java ein eigenes, das im Standardfall dem von Java entspricht. Ein Memory-Modell muss atomare Operationen, die partielle Ordnung von Operationen und die Speichersichtbarkeit berücksichtigen (siehe Kasten "Das Memory-Modell"). C++ geht mit dem Relaxed-Memory-Modell aber noch einen Schritt weiter. Damit erlaubt es die Sprache dem Programmierer, auf maximale Optimierung zu setzen.

Das Memory-Modell

Ein Memory-Modell für eine Programmiersprache muss sich mit folgenden Punkten auseinandersetzen:

1. Atomare Operationen: Operationen, die ohne Unterbrechung ausgeführt werden.

2. Partielle Ordnung von Operationen: Eine Reihenfolge von Operationen, die das System nicht umsortieren darf.

3. Sichtbarkeit des Speichers: Zusicherungen, ab wann gemeinsam genutzte Variablen für einen anderen Thread den gleichen Wert besitzen.

Dieser Artikel bietet nur eine Einführung in das komplexe Thema. Alle Feinheiten kennen nur wenige Experten, die etwa den C++-Compiler oder die Bibliotheken für eine Plattform implementieren.

Undefiniertes Verhalten

Dass bereits ein sehr einfaches Programm mit mehreren Threads der alltäglichen Intuition widerspricht, zeigt das Beispiel in Listing 1. Dieser Artikel wird das Programm im weiteren Verlauf sukzessive verfeinern. Das Programm besitzt zwei Threads namens »thread1« und »thread2« , welche die Funktionen »writing()« in Zeile 6 und »reading()« in Zeile 11 ausführen. Die Funktion »reading()« liest die Werte von »x« und »y« in umgekehrter Reihenfolge.

Listing 1

Unsynchronisiertes Schreiben

01 #include <iostream>
02 #include <thread>
03
04 int x,y;
05
06 void writing(){
07   x= 2000;
08   y= 11;
09 }
10
11 void reading(){
12   std::cout << "y: " << y << " ";
13   std::cout << "x: " << x << std::endl;
14 }
15
16 int main(){
17
18   std::cout << std::endl;
19
20   std::thread thread1(writing);
21   std::thread thread2(reading);
22
23   thread1.join();
24   thread2.join();
25
26   std::cout << std::endl;
27
28 };

Da »writing()« den Wert »x« vor dem Wert »y« schreibt, ist die naive Annahme natürlich, dass mögliche Ausgaben des Programms »(11,2000)« , »(0,2000)« und »(0,0)« sind. Dem ist aber nicht so. Es ist durchaus möglich, dass der Programmlauf den Wert »(11,0)« ergibt. Abbildung 1 stellt die möglichen Ergebnisse des hypothetischen Programmlaufs dar.

Abbildung 1: Mögliche Resultate bei unsynchronisiert ausgeführten Threads.

Es wird sogar noch schlimmer: Da die Variablen »x« und »y« nicht atomar sind, geschieht ihr Lesen und Schreiben ungeschützt, sodass sie beliebige Werte enthalten können. Die Rettung vor solch undefiniertem Verhalten naht mit Locks.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 4 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • Reichhaltiges Angebot

    C++0x bringt nicht nur Veränderungen in der Kernsprache. Die Standardbibliothek der C++-Neuausgabe hat Multithreading, asynchrone Funktionsaufrufe, reguläre Ausdrücke und vieles mehr im Angebot.

  • C++11

    2014 ist ein besonderes Jahr für C++. Drei Jahre nach C++11 erfährt der Sprachstandard mit C++14 den letzten Feinschliff. Neben generischen Lambda-Funktionen und der vereinfachten Ermittlung des Rückgabetyps kann C++14 vor allem mit einem Feature punkten: Reader-Writer-Locks.

  • C++

    Hochperformanten Code zu schreiben, den nur Eingeweihte zu würdigen wissen, ist für Entwickler ein bisschen so, wie für Formel-1-Testpiloten mit einem neuen Motor den Rundenrekord auf dem Nürburgring zu knacken. Doch oft endet der Tuningversuch in der Leitplanke.

  • C++11

    Die C++11-Reihe beschäftigt sich weiter mit dem Synchronisieren von Threads. Diesmal setzt der Chef-Thread Bedingungsvariablen ein, um die Tätigkeit seiner Mitarbeiter-Threads zu koordinieren.

  • C++

    Der neue Standard C++17 kündigt sich für 2017 an. Er wird zwar einige großartige Features wie eine Bibliothek für das Dateisystem mitbringen, doch andere lang ersehnte Funktionen fehlen wohl weiterhin.

comments powered by Disqus

Ausgabe 10/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.