EMZETT.
Login

Kurz: Mit <thread>, <mutex>, <atomic> und <future> führst du Aufgaben parallel aus.

Teil des Kurses C++

Kapitel 12 von 15 im Kurs C++. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

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 fertig

Ein 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
WerkzeugZweck
std::mutex + std::lock_guard / std::scoped_lockgegenseitiger Ausschluss, per RAII automatisch entsperrt
std::atomic<T>atomare Zähler und Flags ohne Sperre
std::condition_variableauf ein Ereignis warten
std::shared_mutexviele Leser, ein Schreiber
thread_localeigene 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: kaputt

Ausnahmen 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: 5050

Regeln für sichere Nebenläufigkeit

  1. Keine gemeinsamen veränderlichen Daten, wo es geht (Nachrichten statt Zustand teilen)
  2. Gemeinsame Daten immer schützen: Mutex, atomic oder unveränderlich machen
  3. Sperren in fester Reihenfolge nehmen oder std::scoped_lock(a, b) verwenden, um Deadlocks zu vermeiden
  4. Kritische Abschnitte kurz halten, keine blockierenden Aufrufe unter Sperre
  5. Datenwettläufe finden: -fsanitize=thread
  6. Parallele Algorithmen: std::sort(std::execution::par, ...) (C++17, <execution>)

Merke

  • std::thread/std::jthread starten Threads; join wartet
  • Gemeinsame Daten mit std::mutex + lock_guard oder std::atomic schützen
  • std::async/future liefern Ergebnisse (auch Ausnahmen) aus anderen Threads
  • condition_variable fü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

Weiter im Kurs

Zurück: Dateien, Streams und Dateisystem

Weiter: Ranges, Module und weitere moderne Features

Alle Kapitel: C++ im Überblick