Werden Daten während ihrer ganze Lebenszeit nur lesend von mehreren Threads verwendet, müssen diese nur sicher initialisiert werden. Das teure Locken der Daten ist bei keinem Zugriff notwendig.
Modernes C++ bietet drei Möglichkeiten an, Daten sicher zu initialisieren.
- Konstante Ausdrücke
- Die Funktion std::call_once in Kombination mit dem Flag std::once_flag
- Statische Variablen mit Blockgültigkeit
Konstante Ausdrücke
Konstante Ausdrücke sind Ausdrücke, die der Compiler zur Übersetzungszeit initialisieren kann. Damit sind sie implizit threadsicher. Durch Voranstellen des Schlüsselwortes constexpr vor den Typ wird dieser zum konstanten Ausdruck.
Aber auch eigene, benutzerdefinierte Typen können konstante Ausdrücke sein. Für diese Typen gelten ein paar Einschränkungen, damit sie zur Übersetzungszeit initialisiert werden können.
- Sie können keine virtuelle Basisklasse besitzen.
- Der Konstruktor muss leer sein und selbst ein konstanter Ausdruck.
- Die Methode kann nicht virtuell sein und muss selbst ein konstanter Ausdruck sein.
Mein Datentyp MyDouble erfüllt alle Bedingungen. Damit lassen sich Instanzen von MyDouble zur Übersetzungszeit erzeugen und sind implizit threadsicher.
struct MyDouble{ constexpr MyDouble(double v): val(v){} constexpr double getValue(){ return val; } private: double val; }; constexpr MyDouble myDouble(10.5); std::cout << myDouble.getValue() << std::endl;
Die Funktion call_once in Kombination mit dem Flag once_flag
Mit der Funktion std::call_once registrieren sie eine aufrufbare Einheit, während sie mit dem std::once_flag sicherstellen, dass die registrierte Funktion genau einmal aufgerufen wird. Damit ist es auch möglich, mehrere Funktionen mit dem gleichen std::once_flag zu annotieren, so dass genau eine Funktion ausgeführt wird.
Das kleine Beispielprogramm zeigt std::call_once und std::once_flag in der Anwendung.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> #include <thread> #include <mutex> std::once_flag onceFlag; void do_once(){ std::call_once(onceFlag, [](){ std::cout << "Only once." << std::endl; }); } int main(){ std::cout << std::endl; std::thread t1(do_once); std::thread t2(do_once); std::thread t3(do_once); std::thread t4(do_once); t1.join(); t2.join(); t3.join(); t4.join(); std::cout << std::endl; } |
In dem Programm werden vier Threads (Zeile 15 - 18) gestartet. Jeder der Threads soll die Funktion do_once ausführen. Das erwartete Ergebnis ist, dass der String "Only once" genau einmal ausgegeben wird.
Legendär ist das Singleton Pattern. Das Singleton Pattern sichert zu, dass genau eine Instanz eines Objektes erzeugt wird. Dies ist eine besondere Herausforderung in Multithreading Programmen. Dank std::call_once und std::once_flag wird die Initialisierung des Singleton Pattern threadsicher.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <iostream> #include <mutex> class MySingleton{ private: static std::once_flag initInstanceFlag; static MySingleton* instance; MySingleton()= default; ~MySingleton()= default; public: MySingleton(const MySingleton&)= delete; MySingleton& operator=(const MySingleton&)= delete; static MySingleton* getInstance(){ std::call_once(initInstanceFlag,MySingleton::initSingleton); return instance; } static void initSingleton(){ instance= new MySingleton(); } }; MySingleton* MySingleton::instance= nullptr; std::once_flag MySingleton::initInstanceFlag; int main(){ std::cout << std::endl; std::cout << "MySingleton::getInstance(): "<< MySingleton::getInstance() << std::endl; std::cout << "MySingleton::getInstance(): "<< MySingleton::getInstance() << std::endl; std::cout << std::endl; } |
Zuerst zum statischen std::once_flag. Dies wird in Zeile 7 erklärt und in Zeile 28 initialisiert. Die statische Methode getInstance (Zeile 16 - 19) verwendet das Flag um zuzusichern, dass die statische Methode initSingleton (Zeile 21 - 23) genau einmal ausgeführt wird. In dieser Methode wird die einzige Instanz der Klasse MySingleton angelegt.
Die Ausgabe des Programms ist unspektakulär. Die MySingleton::getIstance() Methode gibt die Adresse des Singleton-Objekts aus.
Statisch geht es weiter.
Statische Variablen mit Blockgültigkeit
Statische Variablen mit Blockgültigkeit wurden in C++ nur einmal angelegt. Diese Eigenschaft nützt das nach Scott Meyers benannte Meyers Singleton aus um das Singleton Pattern auf sehr elegante Art und Weise umzusetzen.
#include <thread> class MySingleton{ public: static MySingleton& getInstance(){ static MySingleton instance; return instance; } private: MySingleton(); ~MySingleton(); MySingleton(const MySingleton&)= delete; MySingleton& operator=(const MySingleton&)= delete; }; MySingleton::MySingleton()= default; MySingleton::~MySingleton()= default; int main(){ MySingleton::getInstance(); }
Durch das Schlüsselwort default lassen sich die Methoden, die der Compiler bei Bedarf automatisch erzeugt, explizit anfordern. delete hingegen bewirkt, dass die vom Compiler automatisch erzeugten Methoden nicht zur Verfügung stehen. Was hat das Meyers-Singleton mit Multithreading zu tun? Mit C++11 ist die Implementierung auch threadsicher.
Double-Checked Locking Pattern
Wer glaubt, eine weitere Lösung für das sichere Initialisieren eines Singleton-Objekts ist das legendäre Double-Checked Locking Pattern, den muss ich enttäuschen. Das Double-Checked Locking Pattern ist im Allgemeinen keine sichere Art ein Singleton Objekt zu initialisieren. Es geht in der klassischen Form von Annahmen aus, die weder das Java, das C# oder auch das C++ Speichermodell garantiert. Diese Annahme ist, dass der Zugriff auf das Singleton Objekt atomar ist.
Doch was ist Double-Checked Locking Pattern. Eine naheliegende Idee, ein Singleton-Objekt threadsicher zu initialisieren, ist es, seine Initialisierung in ein Lock zu verpacken.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
mutex myMutex; class MySingleton{ public: static MySingleton& getInstance(){ lock_guard<mutex> myLock(myMutex); if( !instance ) instance= new MySingleton(); return *instance; } private: MySingleton(); ~MySingleton(); MySingleton(const MySingleton&)= delete; MySingleton& operator=(const MySingleton&)= delete; static MySingleton* instance; }; MySingleton::MySingleton()= default; MySingleton::~MySingleton()= default; MySingleton* MySingleton::instance= nullptr; |
Problem? Jein. Zwar ist diese Implementierung threadsicher. Aber sie besitzt einen großen Nachteil. Bei jedem, auch nur lesenden Zugriff auf das Singleton-Objekt, wird dieser Zugriff in Zeile 6 durch ein sehr teures Lock geschützt. Oft ist das aber nicht notwendig. Hier setzt das Double-Checked Locking Pattern an.
1 2 3 4 5 6 7 |
static MySingleton& getInstance(){ if ( !instance ){ lock_guard<mutex> myLock(myMutex); if( !instance ) instance= new MySingleton(); } return *instance; } |
Anstelle eines teuren Locks kommt ein billiger Zeigervergleich (Zeile 2) zum Einsatz. Nur wenn dieser Vergleich einen Nullzeiger zurückgibt, findet das teuere Locken des Singleton-Objektes (Zeile 3) statt. Da zwischen dem Zeigervergleich in Zeile 2 und dem Lock in Zeile 3 ein anderer Thread in der Zwischenzeit das Singleton-Objekt bereits initialisiert haben könnte, ist ein zweiter Zeigervergleich in Zeile 4 notwendig. Damit ist der Namen geklärt: Zweimal gecheckt und einmal gelockt oder auch Double-Checked Locking Pattern.
Clever? Ja. Threadsicher? Nein.
Was ist das Problem. Der Aufruf instance= new MySingleton() in Zeile 4 besteht genau betrachtet aus mindestens drei Schritten.
- Allokiere Speicher für MySingleton
- Erzeuge das MySingleton-Objekt in dem Speicher
- Verweise instance auf das MySingleton-Objekt
Diese Reihenfolge sichert der Compiler oder Prozessor aber nicht zu. Es ist aus Optimierungsgründen durchaus zulässig, dass er die Schritte in der Reihenfolge 1, 3 und 2 ausführt. Das bedeutet aber, dass zuerst der Speicher allokiert wird und anschließend instance auf ein unfertiges Singleton-Objekt verweist. Versucht genau zu diesem Zeitpunkt ein anderer Thread auf das Singleton-Objekt zuzugreifen, ergibt der Zeigervergleich in Zeile 2 true. Das heißt, der andere Thread besitzt die Illusion, dass er auf einem fertig initialisierten Singleton-Objekt agiert.
Die Konsequenz ist simpel. Das Programmverhalten ist undefiniert.
Hintergrundinformationen
- Konstante Ausdrücke
- Die Details zu konstanten Ausdrücke können sie in dem online Artikel Konstante Magie 06/2015 nachlesen.
- default und delete
- In dem Artikel Automatik mit Methode 08/2014 gehe ich auf die neuen Schlüsselworte default und delete ein.
Wie geht's weiter?
Um ein Singleton threadsicher zu initialisieren, gibt es sehr viele Variationen. Grob möchte ich diese Variationen in drei Klassen unterteilen.
- Zusicherung des C++-Standards, wie ich sie in diesem Artikel beschrieben habe.
- Locks
- Atomare Operationen
Da stellt sich natürlich die Frage. Welche Strategie soll angewandt werden? Diese Frage kann ich nicht beantworten. Ich will aber helfen, die Antwort zu finden. Im nächsten Artikel werde ich einige Variationen vorstellen, ein Singleton threadsicher zu initialisieren. Darüber hinaus interessiert mich vor allem, wie performant die verschiedenen Variationen sind. Ich bin schon neugierig auf die Ergebnisse.
-
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...