Der Einsatz von Mutexen hört sich sehr einfach an. Es gibt einen kritischen Bereich "Critical Section" im Code, den nun ein Thread betreten darf. Genau diese Exklusivität sichert ein Mutex m durch die zwei Aufrufe m.lock() und m.unlock() zu. Der Teufel steckt aber sprichwörtlich im Detail.
Deadlock
Verwende ich statt des deutschen Begriffs Verklemmung die englischen Begriffe deadlock oder auch deadly embrace, so wird die Geschichte schon dramatischer. Doch was ist eine Verklemmung?
- Verklemmung
- Eine Verklemmung (deadlock, deadly embrace) ist ein Zustand, in dem zwei oder mehrere Threads blockiert sind, da jeder Thread auf die Freigabe der Ressource des anderen Threads wartet, bevor er seine Ressource freigibt.
-
Das Ergebnis der Verklemmung ist vollkommener Stillstand. Der Thread und damit in der Regel das ganze Programm ist für immer blockiert. Verklemmungen lassen sich sehr einfach produzieren. Beispiele gefällig?
Ausnahmen und unbekannter Code
std::mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();
Wirft der unbekannte Code in der Funktion getVar() eine Ausnahme, wird m.unlock() nicht ausgeführt. Jeder Versuch den Mutex m neu anzufordern, schlägt fehl. Das Programm blockiert in diesem Fall für immer. Der Codeschnipsel besitzt aber noch ein weiteres Problem. Während der m.lock() Aufruf aktiv ist, daher der Mutex gelockt ist, wird unbekannter Code in der Funktion getVar() ausgeführt. Was passiert, wenn die Funktion getVar() versucht, den gleichen Mutex ebenfalls anzufordern? Sie ahnen es schon. Eine Verklemmung.
Es geht auch anschaulicher.
Thread 1 und Thread 2 benötigen Zugriff auf zwei Ressourcen um ihre Arbeit zu erledigen. Leider fordern sie die Ressourcen, die durch zwei Mutexe geschützt sind, in verschiedener Reihenfolge an. Arbeiten die zwei Threads zeitlich so verschränkt, dass erst Thread 1 den Mutex 1 lockt, dann Thread 2 den Mutex 2 lockt, befindet sich das Programm im Stillstand. Jeder Thread versucht den anderen Mutex zu locken. Dazu wartet er auf seine Freigabe.
Dies Szenario lässt sich direkt in Code nachbilden.
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
|
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
struct CriticalData{
std::mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
a.mut.lock();
std::cout << "get the first mutex" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
b.mut.lock();
std::cout << "get the second mutex" << std::endl;
// do something with a and b
a.mut.unlock();
b.mut.unlock();
}
int main(){
CriticalData c1;
CriticalData c2;
std::thread t1([&]{deadLock(c1,c2);});
std::thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
}
|
Thread t1 und Thread t2 führen die Funktion deadlock (Zeile 10 - 18) aus. Um deadlock zu prozessieren, benötigt die Funktion die Daten CriticalData c1 und c2 (Zeile 25 und 26) . Da die Objekte c1 und c2 vor gleichzeitigem Zugriff mehrerer Threads geschützt werden müssen, besitzen die Objekte jeweils ein Mutex. Der Einfachheit halber bieten c1 und c2 keine Funktionalität an.
Nur ein kurzes Schlafen in Zeile 14 reicht aus um die Verklemmung zu provozieren.
Die einzige Möglichkeit das Programm zu beenden ist ein beherztes Strg C.
Wie geht's weiter?
Zugegeben, die vorgestellten Beispiele tragen nicht unbedingt dazu bei, Multithreading Programme mit Zuversicht zu schreiben. Zumal die Komplexität eines Programms gefühlt mit jedem Mutex quadratisch steigt. Dafür gibt es aber im nächsten Artikel die Lösung in Form von Locks, die Mutexe auf sichere Weise kapseln.
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...