Die große Herausforderung mit Threads beginnt dann, wenn sich diese Daten teilen und diese nicht konstant sind.
Kritischer Wettlauf und kritischer Bereich
Im Zusammenhang mit von Threads gemeinsam verwendeten Daten wird gerne von einem kritischen Wettlauf oder auch kritischem Bereich gesprochen. Doch was ist das?
- Kritischer Wettlauf (Race Condition)
- Ein kritischer Wettlauf entsteht genau dann, wenn mindestens zwei Threads gleichzeitig ein Datum verwenden, wobei zumindestens ein Thread versucht, diese zu modifizieren.
- Kritischer Bereich (Critial Section)
- Ein kritischer Bereich ist ein zusammenhängender Datenbereich, in dem nur ein Thread Zugriff auf die Daten haben darf.
-
Enthält ein Programm einen kritischen Wettlauf, dann ist das Programmverhalten undefiniert. Anders ausgedrückt, jedes erdenkliche und unerdenkliche Verhalten ist möglich.
-
Schön lässt sich dies verschränkte Ausführen von Threads visualisieren, wenn mehrere Threads versuchen, gleichzeitig auf std::cout zu schreiben. std::cout ist daher das gemeinsame Datum, dass es vor dem gleichzeitigen modifizierenden Zugriff mehrerer Threads zu schützen gilt.
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 40 41 42 43 44 45 46 47
#include <chrono> #include <iostream> #include <thread> class Worker{ public: Worker(std::string n):name(n){}; void operator() (){ for (int i= 1; i <= 3; ++i){ // begin work std::this_thread::sleep_for(std::chrono::milliseconds(200)); // end work std::cout << name << ": " << "Work " << i << " done !!!" << std::endl; } } private: std::string name; }; int main(){ std::cout << std::endl; std::cout << "Boss: Let's start working.\n\n"; std::thread herb= std::thread(Worker("Herb")); std::thread andrei= std::thread(Worker(" Andrei")); std::thread scott= std::thread(Worker(" Scott")); std::thread bjarne= std::thread(Worker(" Bjarne")); std::thread andrew= std::thread(Worker(" Andrew")); std::thread david= std::thread(Worker(" David")); herb.join(); andrei.join(); scott.join(); bjarne.join(); andrew.join(); david.join(); std::cout << "\n" << "Boss: Let's go home." << std::endl; std::cout << std::endl; }
Der Boss gibt seinen sechs Arbeitern (Zeile 29 - 34) jeweils drei Arbeitspakete in den Zeilen 9 - 15. Hat ein Arbeiter sein Arbeitspaket erledigt, schreit er dies zum Boss (main-Thread) zurück (Zeile 14). Hat der Boss alle Benachrichtigungen von seinen Arbeitern erhalten, schickt er sie in den wohlverdienten Feierabend (Zeile 43).
Was für ein Durcheinander!
Am nächsten Tag wird es auch nicht besser. Die Arbeiter schreien wiederum vollkommen unkoordiniert durcheinander.
-
Die naheliegende Möglichkeit, den Zugriff der Threads auf die gemeinsame Variable std::cout zu synchronisieren, ist ein Mutex.
Exkurs: std::cout ist threadsicher
- Der C++11 Standard sicher zu, dass die einzelnen Zeichen auf den Ausgabestream std::cout vor gleichzeitiger Modifikation mehrerer Threads nicht geschützt werden müssen. Jedes Zeichen wird atomar ausgegeben. Natürlich können mehrere Ausgabeanweisungen wie in dem Beispiel beliebig vermischt werden. Das ist aber nur ein optisches Problem. Das Programm ist wohldefiniert. Diese Aussage gilt neben den Ausgabestream std::cout für alle weiteren Ein- und Ausgabebstreams in C++.
-
Mutex
Ein Mutex steht für Mutual Exclusion. Dieser sichert zu, dass der kritische Bereich exklusiv von einem Thread verwendet wird.
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 40 41 42 43 44 45 46 47 48 49 50 51
#include <chrono> #include <iostream> #include <mutex> #include <thread> std::mutex coutMutex; class Worker{ public: Worker(std::string n):name(n){}; void operator() (){ for (int i= 1; i <= 3; ++i){ // begin work std::this_thread::sleep_for(std::chrono::milliseconds(200)); // end work coutMutex.lock(); std::cout << name << ": " << "Work " << i << " done !!!" << std::endl; coutMutex.unlock(); } } private: std::string name; }; int main(){ std::cout << std::endl; std::cout << "Boss: Let's start working." << "\n\n"; std::thread herb= std::thread(Worker("Herb")); std::thread andrei= std::thread(Worker(" Andrei")); std::thread scott= std::thread(Worker(" Scott")); std::thread bjarne= std::thread(Worker(" Bjarne")); std::thread andrew= std::thread(Worker(" Andrew")); std::thread david= std::thread(Worker(" David")); herb.join(); andrei.join(); scott.join(); bjarne.join(); andrew.join(); david.join(); std::cout << "\n" << "Boss: Let's go home." << std::endl; std::cout << std::endl; }
Der entscheidende Unterschied zum ersten nicht synchronisierten Programm sind in diesem Programm die Zeilen 17 und 19. Durch die Methodenaufrufe coutMutex.lock() und coutMutex.unlock() wird der kritische Bereich erklärt, der nur von einem Thread zu einem Zeitpunkt verwendet werden darf. Dadurch ist der Zugriff auf den Ausgabestream std::cout synchronisiert. Nun löst sich das Geschrei der Arbeiter in Wohlgefallen auf.
Mutexe bergen ihre eigenen Gefahren. Dazu aber mehr im nächsten Artikel.
Hintergrundinformationen
C++11 kennt mehrere Variationen von Mutexen. So gibt es Mutexe, die rekursiv aufgerufen oder mit Zeitbedingungen verwendet werden können.
C++14 führt einen weiteren Mutex ein, der von mehreren Threads gleichzeitig verwendet werden kann. Dieser Mutex bildet die Grundlage für Reader-Writer Locks. Die Details zu den Mutexvarianten lassen sich schön auf en.cppreference.com nachlesen.
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...