Kurz: Mit <thread>, <mutex>, <atomic> und <future> führst du Aufgaben parallel aus.
Teil des Kurses C++
Mit <thread>, <mutex>, <atomic> und <future> führst du Aufgaben parallel aus.
Threads
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
void arbeite(int id) {
std::this_thread::sleep_for(std::chrono::milliseconds(10 * id));
std::cout << "Thread " << id << " fertig\n";
}
int main() {
std::thread t1(arbeite, 1);
std::thread t2(arbeite, 2);
t1.join(); // warten, bis der Thread fertig ist
t2.join();
std::cout << "alle fertig\n";
std::vector<std::jthread> pool; // jthread (C++20): joined automatisch im Destruktor
for (int i = 0; i < 3; i++) pool.emplace_back([i] { (void) i; });
return 0;
}Ausgabe:
Thread 1 fertig
Thread 2 fertig
alle fertigEin std::thread muss vor der Zerstörung join()ed oder detach()ed werden, sonst bricht das Programm ab. std::jthread (C++20) macht das automatisch.
Wettlaufsituationen und Mutex
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <vector>
int main() {
int ungeschuetzt = 0;
int geschuetzt = 0;
std::atomic<int> atomar{0};
std::mutex sperre;
auto aufgabe = [&] {
for (int i = 0; i < 100000; i++) {
++ungeschuetzt; // Datenwettlauf (undefiniertes Verhalten!)
{ std::lock_guard<std::mutex> g(sperre); ++geschuetzt; }
++atomar;
}
};
std::vector<std::thread> threads;
for (int i = 0; i < 4; i++) threads.emplace_back(aufgabe);
for (auto& t : threads) t.join();
std::cout << geschuetzt << ' ' << atomar << '\n'; // exakt 400000
(void) ungeschuetzt; // wäre unzuverlässig
return 0;
}Ausgabe:
400000 400000| Werkzeug | Zweck |
|---|---|
std::mutex + std::lock_guard / std::scoped_lock | gegenseitiger Ausschluss, per RAII automatisch entsperrt |
std::atomic<T> | atomare Zähler und Flags ohne Sperre |
std::condition_variable | auf ein Ereignis warten |
std::shared_mutex | viele Leser, ein Schreiber |
thread_local | eigene Kopie pro Thread |
Verwende nie mutex.lock()/unlock() direkt: lock_guard gibt auch bei Ausnahmen frei.
async und future: Ergebnisse berechnen lassen
#include <iostream>
#include <future>
#include <numeric>
#include <vector>
long summe(const std::vector<int>& v, std::size_t von, std::size_t bis) {
return std::accumulate(v.begin() + von, v.begin() + bis, 0L);
}
int main() {
std::vector<int> z(1000000);
std::iota(z.begin(), z.end(), 1);
auto haelfte1 = std::async(std::launch::async, summe, std::cref(z), 0, z.size() / 2);
auto haelfte2 = std::async(std::launch::async, summe, std::cref(z), z.size() / 2, z.size());
std::cout << haelfte1.get() + haelfte2.get() << '\n'; // get() wartet auf das Ergebnis
std::promise<int> versprechen;
std::future<int> zukunft = versprechen.get_future();
std::thread t([&versprechen] { versprechen.set_value(42); });
std::cout << zukunft.get() << '\n';
t.join();
auto fehler = std::async([]() -> int { throw std::runtime_error("kaputt"); });
try { fehler.get(); } catch (const std::exception& e) { std::cout << "Fehler im Thread: " << e.what() << '\n'; }
return 0;
}Ausgabe:
500000500000
42
Fehler im Thread: kaputtAusnahmen in einem async-Thread werden beim get() weitergegeben.
Producer und Consumer
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
int main() {
std::queue<int> warteschlange;
std::mutex m;
std::condition_variable cv;
bool fertig = false;
std::thread konsument([&] {
int summe = 0;
for (;;) {
std::unique_lock lock(m);
cv.wait(lock, [&] { return !warteschlange.empty() || fertig; });
while (!warteschlange.empty()) { summe += warteschlange.front(); warteschlange.pop(); }
if (fertig) break;
}
std::cout << "Summe: " << summe << '\n';
});
for (int i = 1; i <= 100; i++) {
{ std::lock_guard g(m); warteschlange.push(i); }
cv.notify_one();
}
{ std::lock_guard g(m); fertig = true; }
cv.notify_one();
konsument.join();
return 0;
}Ausgabe:
Summe: 5050Regeln für sichere Nebenläufigkeit
- Keine gemeinsamen veränderlichen Daten, wo es geht (Nachrichten statt Zustand teilen)
- Gemeinsame Daten immer schützen: Mutex,
atomicoder unveränderlich machen - Sperren in fester Reihenfolge nehmen oder
std::scoped_lock(a, b)verwenden, um Deadlocks zu vermeiden - Kritische Abschnitte kurz halten, keine blockierenden Aufrufe unter Sperre
- Datenwettläufe finden:
-fsanitize=thread - Parallele Algorithmen:
std::sort(std::execution::par, ...)(C++17,<execution>)
Merke
std::thread/std::jthreadstarten Threads;joinwartet- Gemeinsame Daten mit
std::mutex+lock_guardoderstd::atomicschützen std::async/futureliefern Ergebnisse (auch Ausnahmen) aus anderen Threadscondition_variablefür Warten auf Ereignisse- ThreadSanitizer findet Datenwettläufe
Übungsaufgabe
Teile die Summe von 1 bis 10 Millionen auf vier std::async-Aufgaben auf und addiere die Teilsummen.
Quiz zur Selbstkontrolle
Was passiert, wenn ein std::thread ohne join zerstört wird?
- Das Programm wird beendet (std::terminate) (richtig)
- Der Thread läuft im Hintergrund weiter
- Nichts
- Der Thread wird pausiert
Wozu dient lock_guard?
- Sperrt einen Mutex und gibt ihn automatisch frei (RAII) (richtig)
- Verschlüsselt Daten
- Startet Threads
- Löscht Variablen
Wie bekommst du das Ergebnis einer async-Aufgabe?
- future.get() (richtig)
- future.value
- thread.result()
- async.return()
Weiter im Kurs
Zurück: Dateien, Streams und Dateisystem
Weiter: Ranges, Module und weitere moderne Features
Alle Kapitel: C++ im Überblick