Přeskočit na obsah

Stavové proměnné a výrobce-spotřebitel

Použijte proměnné podmínky k efektivní koordinaci vláken. Vybudujte vláknově bezpečnou frontu producent-spotřebitel, zpracujte falešná probuzení a vyhněte se ztraceným problémům s probuzením.

Koordinace vláken s podmínkovými proměnnými

proměnná podmínky umožňuje vláknu čekat, dokud se některá podmínka nestane pravdivou, bez zaneprázdněné smyčky. Namísto otáčeníwhile (!ready) { /* burn CPU */ }vlákno spí a je probuzeno jiným vláknem, které volánotify_one()nebonotify_all().

std::condition_variablefunguje pouze sstd::unique_lock. Vzor je vždy:

1. Uzamkněte mutex.
2. Zkontrolujte podmínku — pokud je false, zavolejtewait(), který atomicky uvolní mutex a pozastaví vlákno.
3. Po upozornění (a podmínka je pravdivá) je mutex znovu získán a provádění pokračuje.

Oznamující vlákno upraví sdílený stav pod stejným mutexem a poté zavolánotify_one()nebonotify_all().

Základní stav Variabilní použití

Zde je základní vzorec: jedno vlákno čeká na data, jiné je poskytuje.

condition_variable_basic.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <string>

std::mutex mtx;
std::condition_variable cv;
std::string data;
bool ready = false;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    // Wait until ready is true (handles spurious wakeups)
    cv.wait(lock, []{ return ready; });
    std::cout << "Consumer received: " << data << "\n";
}

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        data = "Hello from producer";
        ready = true;
    }  // unlock before notify to avoid waking thread into contention
    cv.notify_one();
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);
    t1.join();
    t2.join();
    return 0;
}
Úskalí

Proměnná podmínky může probudit vlákno i když nebylo odesláno žádné oznámení – tomu se říká nepravé probuzení. Je to povoleno standardem C++ (a běžné na skutečných systémech) z důvodů výkonu. Musíte vždy použít predikátový tvarwait()nebo jej zabalit do smyčkywhile:

Smyčka ```cpp
// CORRECT — predicate form handles spurious wakeups
cv.wait(lock, []{ return ready; });

// EQUIVALENT manual loop
while (!ready) {
cv.wait(lock);
}

// WRONG — wakes up randomly, proceeds without data
cv.wait(lock); // no condition check!
```

Never call cv.wait(lock) without a predicate or a surrounding while.

Thread-Safe Bounded Buffer (výrobce-spotřebitel)

Vzor producent-spotřebitel je jedním z nejběžnějších vzorů souběžnosti. Producenti přidávají položky do sdílené vyrovnávací paměti; spotřebitelé je odstraní. Ohraničená vyrovnávací paměť blokuje producenty, když jsou plné, a spotřebitele, když jsou prázdné.

bounded_queue.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>

template <typename T>
class BoundedQueue {
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable not_full_;
    std::condition_variable not_empty_;
    size_t capacity_;
public:
    explicit BoundedQueue(size_t cap) : capacity_(cap) {}

    void push(T item) {
        std::unique_lock<std::mutex> lock(mtx_);
        not_full_.wait(lock, [this]{ return queue_.size() < capacity_; });
        queue_.push(std::move(item));
        not_empty_.notify_one();
    }

    T pop() {
        std::unique_lock<std::mutex> lock(mtx_);
        not_empty_.wait(lock, [this]{ return !queue_.empty(); });
        T item = std::move(queue_.front());
        queue_.pop();
        not_full_.notify_one();
        return item;
    }
};

int main() {
    BoundedQueue<int> bq(5);  // capacity 5

    // Producer
    std::thread producer([&bq]() {
        for (int i = 0; i < 20; ++i) {
            bq.push(i);
            std::cout << "Produced: " << i << "\n";
        }
    });

    // Consumer
    std::thread consumer([&bq]() {
        for (int i = 0; i < 20; ++i) {
            int val = bq.pop();
            std::cout << "Consumed: " << val << "\n";
        }
    });

    producer.join();
    consumer.join();
    return 0;
}

Problém ztraceného probuzení

ztracené probuzení nastane, když je volánonotify_one()předtím, než spotřebitel zavoláwait(). Vzhledem k tomu, že žádné vlákno nečeká, oznámení se ztratí – spotřebitel pak navždy spí.

Standardní vzor tomu zabrání kontrolou predikátu před spaním. Pokud již producent nastavil podmínku na hodnotu true, predikát vrátí hodnotu true await()se vrátí okamžitě bez spánku. To je důvod, proč je predikátový tvarcv.wait(lock, pred)nezbytný — kontroluje stav před a po každém probuzení:

1. Uzamkněte mutex.
2. Zkontrolujte predikát — pokud je již pravdivý, přeskočte čekání.
3. Jinak atomicky odemkněte a uspěte.
4. Po probuzení znovu zamkněte a znovu zkontrolujte predikát.

Dokud bude výrobce i spotřebitel chránit sdílený příznak pomocí stejného mutexu, nelze ztratit žádné probuzení.

notify_one vs notify_all

notify_one()probudí jedno čekající vlákno;notify_all()je probudí všechny. Použijtenotify_all(), když může více vláken potřebovat znovu zkontrolovat svůj stav (např. signály vypnutí), anotify_one(), když by mělo pokračovat přesně jedno vlákno (např. jeden zákazník přebírá úlohu).

notify_all_shutdown.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <atomic>

std::mutex mtx;
std::condition_variable cv;
bool shutdown = false;

void worker(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return shutdown; });
    std::cout << "Worker " << id << " shutting down\n";
}

int main() {
    std::vector<std::thread> workers;
    for (int i = 0; i < 5; ++i) {
        workers.emplace_back(worker, i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(1));

    {
        std::lock_guard<std::mutex> lock(mtx);
        shutdown = true;
    }
    cv.notify_all();  // wake ALL workers for shutdown

    for (auto& w : workers) w.join();
    return 0;
}
Osvědčený postup

Vždy používejte predikátové přetíženíwait()k ochraně před falešným probuzením a ztraceným probuzením. Odemkněte mutex před volánímnotify_one()/notify_all(), když je to možné — zabráníte tím okamžitému zablokování probuzeného vlákna na mutexu. Preferujtenotify_one(), když se potřebuje probudit pouze jedno vlákno, anotify_all()pro scénáře vysílání, jako je vypnutí. Nikdy nečekejte na proměnnou podmínky slock_guard– potřebujeteunique_lock.

Klíčové poznatky
  • std::condition_variablenechává vlákna spát, dokud není podmínka splněna, čímž se vyhne drahému čekání
  • Vždy používejte predikátovou formuwait()ke zpracování falešných probuzení i ztracených probuzení
  • Vzor výrobce-spotřebitel používá dvě proměnné podmínky: „není plná“ a „není prázdná“
  • notify_one()probudí jednoho číšníka;notify_all()probudí všechny číšníky – použijte ten správný pro svůj scénář
  • Číšník i oznamovatel musí k ochraně sdíleného stavu použít stejný mutex

Kvíz — Otestujte své znalosti

(20 XP)

1. Proč musíte vždy používat predikát s`condition_variable::wait()`?

2. Co by měl výrobce v omezené frontě producent-spotřebitel dělat, když je fronta plná?

3. Jaký typ zámku vyžaduje`std::condition_variable::wait()`?