std::thread, std::jthread & Thread Management
Naučte se vytvářet a spravovat vlákna pomocí std::thread a std::jthread C++20. Pochopte spojení vs odpojení, kooperativní zrušení s tokeny zastavení, místní úložiště vláken a zpracování výjimek napříč hranicemi vláken.
Proč Concurrency?
Moderní procesory mají více jader a aby je mohl váš program využívat, musí pracovat současně. C++11 představil přenosnou knihovnu vláken va C++20 ji vylepšil pomocístd::jthread. vlákno je nezávislá cesta provádění, která sdílí stejný adresní prostor jako vlákno, které jej vytvořilo – což je jeho síla i nebezpečí.
Na rozdíl od procesů vlákna sdílejí paměť. Díky tomu je komunikace rychlá, ale také zavádí závodní data, pokud dvě vlákna čtou a zapisují stejnou proměnnou bez synchronizace. Než se pustíme do synchronizace (další lekce), naučte se nejprve vytvářet, spravovat a čistě zavírat vlákna.
Vytváření vláken pomocí std::thread
std::threadvytvoříte tak, že mu předáte callable (funkce, lambda, funktor) a jakékoli argumenty. Vlákno se začne spouštět ihned po sestavení.
#include <iostream>
#include <thread>
void greet(const std::string& name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
}
}
int main() {
// Launch a thread running greet("Alice", 3)
std::thread t1(greet, "Alice", 3);
// Lambda-based thread
std::thread t2([]() {
std::cout << "Running in a lambda thread\n";
});
// MUST join or detach before t1/t2 are destroyed
t1.join(); // block until t1 finishes
t2.join();
std::cout << "All threads finished\n";
return 0;
}join() vs odpojit()
Každéstd::thread, které představuje běžící vlákno, musí být před spuštěním jeho destruktoru připojeno nebo odpojeno. Pokud zapomenete, zavolá sestd::terminate()a váš program se zhroutí.
join() blokuje volající vlákno, dokud cílové vlákno neskončí. Toto je bezpečné výchozí nastavení – víte, že vlákno je hotové a jeho výsledky jsou připraveny.
detach() přeruší spojení mezi objektemstd::threada vláknem OS. Vlákno běží na pozadí ("vlákno démona") a vy se do něj již nemůžete připojit. Buďte opatrní: pokud odpojené vlákno přistupuje k proměnným zásobníku z funkce spawnování a tato funkce se vrací, dostanete nedefinované chování.
Nikdydetach()vlákno, které zachycuje nebo odkazuje na místní proměnné z rozsahu vytváření. Když se funkce vytváření vrátí, tito místní jsou zničeni a odpojené vlákno přistupuje k visící paměti. Toto je nedefinované chování a často se projevuje jako sporadické pády. Pokud se musíte odpojit, předejte data podle hodnoty nebo použijtestd::shared_ptrk prodloužení životnosti.
std::jthread — RAII vlákno (C++20)
std::jthreadřeší dva problémy sstd::thread: (1) automaticky se připojí ke svému destruktoru, takže nikdy nezapomenete, a (2) podporuje kooperativní zrušení prostřednictvímstd::stop_token.
#include <iostream>
#include <thread>
#include <chrono>
void worker(std::stop_token stoken, int id) {
while (!stoken.stop_requested()) {
std::cout << "Worker " << id << " running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Worker " << id << " stopping gracefully\n";
}
int main() {
std::jthread jt1(worker, 1); // stop_token is auto-passed as first arg
std::jthread jt2(worker, 2);
std::this_thread::sleep_for(std::chrono::seconds(1));
// Request both threads to stop
jt1.request_stop();
jt2.request_stop();
// No need to call join() — jthread's destructor does it
std::cout << "Main thread exiting\n";
return 0;
}Dotazování hardwaru a místního úložiště vláken
std::thread::hardware_concurrency()vrací počet souběžných vláken, která hardware podporuje. Doba ukládáníthread_localposkytuje každému vláknu jeho vlastní kopii proměnné.
#include <iostream>
#include <thread>
#include <vector>
thread_local int tls_counter = 0; // each thread gets its own copy
void count_work(int id) {
for (int i = 0; i < 1000; ++i) {
++tls_counter; // no data race — each thread has its own
}
std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}
int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << n << " threads\n";
std::vector<std::thread> threads;
for (unsigned int i = 0; i < n; ++i) {
threads.emplace_back(count_work, i);
}
for (auto& t : threads) {
t.join();
}
// Each thread printed 1000 — they don't share tls_counter
return 0;
}V C++20 a novějších upřednostňujtestd::jthreadpředstd::thread. Automaticky se připojí ke svému destruktoru, čímž se eliminuje riziko volánístd::terminate, pokud se zapomenete připojit. Jeho vestavěný mechanismusstd::stop_tokenposkytuje čistý, standardizovaný způsob, jak požádat o kooperativní zrušení – není třeba vytvářet vlastní příznakatomic.
Zpracování výjimek napříč vlákny
Výjimka vyvolaná uvnitř vlákna, která není zachycena v rámci tohoto vlákna, volástd::terminate(). Výjimky se ne automaticky rozšíří do připojovaného vlákna.
Chcete-li přenést výjimky mezi vlákny, zachyťte je pomocístd::current_exception()uvnitř pracovního vlákna a uložte je dostd::exception_ptr. Spojovací vlákno pak může být znovu vyvoláno pomocístd::rethrow_exception(). Mechanismusstd::future(popsaný v lekci 4) to zvládá automaticky.
std::threadse spustí okamžitě; musítejoin()nebodetach()před spuštěním jeho destruktorustd::jthread(C++20) se automaticky připojí ke svému destruktoru a podporuje kooperativní zrušení prostřednictvímstd::stop_tokenthread_localdává každému vláknu jeho vlastní nezávislou kopii proměnné – není potřeba žádná synchronizacestd::thread::hardware_concurrency()vám říká, kolik vláken může hardware běžet souběžně- Výjimky ve vláknu se na volajícího nepřenesou — k jejich přenosu použijte
std::exception_ptrnebostd::future - Nikdy nedovolte, aby odpojené vlákno odkazovalo na místní proměnné z rozsahu vytváření
Kvíz — Otestujte své znalosti
(15 XP)1. Co se stane, když je objekt`std::thread`představující připojitelné vlákno zničen bez volání`join()`nebo`detach()`?
2. Jak`std::jthread`podporuje kooperativní zrušení?
3. Co dělá klíčové slovo`thread_local`?