Table of Contents
Was ist multithreading?
Multithreading ist die Fähigkeit eines Prozessors, mehrere Threads gleichzeitig auszuführen.
Ein Thread ist ein Pfad der Ausführung, der es einem Prozessor ermöglicht, mehrere Aufgaben gleichzeitig auszuführen.
Die meisten Prozessoren können mehrere Threads gleichzeitig ausführen.
Beispiel mit einem (zusätzlichem) Thread
In diesem Beispiel wird ein zusätzlicher Thread gestartet, der eine Funktion ausführt.
Um zu zeigen, dass die Ausgabe quasi gleichzeitig erfolgt, wird in der main-Funktion eine for-Schleife ausgeführt, die ein Minus ausgibt.
Die Funktion worker_function() gibt ein Plus aus.
Die Ausgabe sieht dann so aus:
+++———-+++++++
oder
+++++++———-+++
oder
+++++———-+++++
Die Ausgabe ist nicht vorhersehbar, da die Threads quasi gleichzeitig ausgeführt werden.
#include <iostream>
#include <thread>
void worker_function()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "+";
}
}
int main(){
std::thread t1(worker_function);
for (size_t i = 0; i < 10; i++)
{
std::cout << "-";
}
t1.join();
}
Race condition
Dass die Ausgabe quasi gleichzeitig erfolgt, kann zu einem Problem führen.
In diesem Beispiel wird eine globale Variable von zwei Threads hochgezählt.
Die Ausgabe sollte 200.000 sein, ist es aber nicht.
#include <iostream>
#include <thread>
namespace
{
auto GLOBAL_COUNTER = std::uint32_t{0};
}
void worker_function()
{
for (size_t i = 0; i < 100000; i++)
{
GLOBAL_COUNTER = GLOBAL_COUNTER + 1;
}
}
int main(){
std::thread t1(worker_function);
std::thread t2(worker_function);
t1.join();
t2.join();
std::cout << "Global counter: " << GLOBAL_COUNTER << std::endl;
}
In der Theorie sollte der Counter immer bis 200.000 hochgezählt werden. In der Praxis ist das nicht der Fall.
Das liegt daran, dass die beiden Threads gleichzeitig auf die Variable zugreifen und die Operationen nicht atomar sind.
Das heißt, dass die Operationen nicht in einem Schritt ausgeführt werden, sondern in mehreren.
Dies führt dazu, dass die Threads sich gegenseitig überschreiben und der Counter nicht bis 200.000 hochzählt.
Mutex
Aber es gibt eine Lösung für dieses Problem.
Die Schleife, die den Counter hochzählt, wird in einen kritischen Bereich verschoben.
Das heißt, dass die Schleife nur von einem Thread gleichzeitig ausgeführt werden kann.
Der kritische Bereich wird mit einem sogenannten Mutex geschützt.
Mutex steht für mutual exclusion und ist ein Objekt, das den Zugriff auf einen kritischen Bereich regelt.
#include <iostream>
#include <thread>
#include <mutex>
namespace
{
auto GLOBAL_COUNTER = std::uint32_t{0};
std::mutex myMutex;
}
void worker_function()
{
myMutex.lock();
for (size_t i = 0; i < 100000; i++)
{
GLOBAL_COUNTER = GLOBAL_COUNTER + 1;
}
myMutex.unlock();
}
lock guard
Anstatt den mutex zu locken und zu unlocken, benutzen wir einen lock guard.
Der lock guard lockt den mutex, wenn er erstellt wird und unlockt ihn, wenn er zerstört wird.
Das heißt, dass der mutex automatisch unlockt wird, wenn die Funktion verlassen wird.
Das ist wichtig, da sonst der mutex gelockt bleibt, wenn eine exception geworfen wird.
void worker_function()
{
std::lock_guard guard(myMutex);
for (size_t i = 0; i < 100000; i++)
{
GLOBAL_COUNTER = GLOBAL_COUNTER + 1;
}
}