Multithreading v C++/CLI
Podrobný průvodce pro začátečníky – od prvního vlákna po synchronizaci a TPL
1. Co je vlákno (Thread)?
Program, který spustíme, běží jako proces. Každý proces má nejméně jedno vlákno – hlavní vlákno (main thread). Vlákno je nejmenší jednotka provádění kódu, kterou operační systém může plánovat na CPU.
Proč vůbec vlákna?
- Paralelismus – na vícejádrovém CPU běží více vláken skutečně současně.
- Responzivita UI – dlouhá operace na pozadí nezmrazí okno.
- Čekání na IO – jedno vlákno čeká na disk/síť, ostatní počítají.
2. Managed vs. Native vlákna v C++/CLI
C++/CLI je jazyk, který propojuje nativní C++ s .NET (managed) světem. Pro vlákna máte dvě volby:
| Typ | Hlavičky | Použití |
|---|---|---|
Managed (System::Threading) |
#using <mscorlib.dll> (automaticky) |
Doporučeno – GC, výjimky, interop s .NET |
Native (std::thread, Win32) |
#include <thread> |
Pro čistě nativní kód bez CLR |
Tento průvodce se zaměřuje na managed vlákna přes System::Threading, protože jsou plně integrována s .NET ekosystémem a jsou preferovanou volbou v C++/CLI projektech.
Vlastnosti → Obecné → Podpora CLR: Podpora CLR (/clr).
3. Přehled System::Threading
Jmenný prostor System::Threading nabízí vše potřebné:
| Třída / typ | Co dělá |
|---|---|
Thread | Nízkoúrovňové vlákno, plná kontrola |
ThreadPool | Fond recyklovaných vláken – lehčí na zdroje |
Task | Vyšší abstrakce (TPL), async/await vzor |
Monitor | Základní synchronizační primitiv (lock) |
Mutex | Výhradní přístup, i mezi procesy |
Semaphore | Omezený počet souběžných přístupů |
AutoResetEvent | Signalizace mezi vlákny |
Interlocked | Atomické operace s čísly |
CancellationToken | Kooperativní zrušení vlákna/tasku |
4. Třída Thread – vytvoření prvního vlákna
4.1 Nejjednodušší příklad
#using <mscorlib.dll> using namespace System; using namespace System::Threading; // Funkce, která poběží v novém vlákně void MojeVlakno() { for (int i = 0; i < 5; i++) { Console::WriteLine("Vlákno: iterace {0}", i); Thread::Sleep(300); // čekání 300 ms } } int main() { // 1) Vytvoříme delegáta na naši funkci ThreadStart^ del = gcnew ThreadStart(&MojeVlakno); // 2) Vytvoříme Thread objekt Thread^ t = gcnew Thread(del); // 3) Spustíme vlákno t->Start(); // Hlavní vlákno mezitím dělá svou práci for (int i = 0; i < 3; i++) { Console::WriteLine("Hlavní: iterace {0}", i); Thread::Sleep(500); } // 4) Počkáme, až vlákno skončí t->Join(); Console::WriteLine("Hotovo."); return 0; }
Hlavní: iterace 0 Vlákno: iterace 0 Vlákno: iterace 1 Hlavní: iterace 1 Vlákno: iterace 2 Vlákno: iterace 3 Hlavní: iterace 2 Vlákno: iterace 4 Hotovo.
Join() zablokuje volající vlákno, dokud se cílové vlákno neukončí. Bez Join() by program mohl skončit dříve, než vlákno stihne svou práci.
4.2 Předání parametru vláknu
Chceme-li předat data, použijeme ParameterizedThreadStart a Object^:
void VlaknoSParametrem(Object^ param) { String^ zprava = safe_cast<String^>(param); Console::WriteLine("Vlákno dostalo: {0}", zprava); } int main() { ParameterizedThreadStart^ del = gcnew ParameterizedThreadStart(&VlaknoSParametrem); Thread^ t = gcnew Thread(del); t->Start("Ahoj z hlavního vlákna!"); t->Join(); return 0; }
4.3 Vlákno jako metoda třídy (ref class)
Nejčistší způsob – metoda objektu jako vstupní bod vlákna:
ref class Zpracovavac { private: int _pocet; public: Zpracovavac(int pocet) : _pocet(pocet) {} void Spust() // vstupní bod vlákna { for (int i = 0; i < _pocet; i++) { Console::WriteLine("Zpracovávám položku {0}", i); Thread::Sleep(200); } } }; int main() { Zpracovavac^ z = gcnew Zpracovavac(5); Thread^ t = gcnew Thread( gcnew ThreadStart(z, &Zpracovavac::Spust) ); t->Start(); t->Join(); return 0; }
4.4 Vlastnosti vlákna
t->Name = "MojeVlakno"; // jméno (vidíte v debuggeru) t->Priority = ThreadPriority::BelowNormal; // priorita t->IsBackground = true; // ukončí se s hlavním vláknem Console::WriteLine(t->ThreadState); // Running, WaitSleepJoin, Stopped... Console::WriteLine(t->IsAlive); // true dokud běží
IsBackground = true.
5. ThreadPool – fond vláken
Vytváření a ničení vláken je relativně drahé. ThreadPool udržuje zásobu recyklovaných vláken – stačí zařadit úkol do fronty.
void UkolZFronty(Object^ stav) { int id = safe_cast<int>(stav); Console::WriteLine("Úkol {0} spustilo vlákno #{1}", id, Thread::CurrentThread->ManagedThreadId); Thread::Sleep(500); } int main() { // Zařadíme 5 úkolů do fondu for (int i = 0; i < 5; i++) ThreadPool::QueueUserWorkItem( gcnew WaitCallback(&UkolZFronty), i); Thread::Sleep(2000); // počkáme na dokončení return 0; }
- Nelze nastavit prioritu ani name jednotlivého vlákna.
- Nelze volat
Join()– pro čekání na dokončení použijteCountdownEventneboTask. - Nevhodné pro dlouhé blokující operace (vyčerpáte fond).
6. Task Parallel Library (TPL)
TPL je moderní a preferovaný přístup od .NET 4.0. Task je abstrakce nad ThreadPoolem s podporou pro výsledky, skládání a zrušení.
6.1 Základní Task
#using <System.Threading.Tasks.dll> using namespace System::Threading::Tasks; int main() { // Lambda výraz jako úkol Task^ t = Task::Run( gcnew Action([]() { Console::WriteLine("Task běží na vlákně #{0}", Thread::CurrentThread->ManagedThreadId); Thread::Sleep(1000); }) ); Console::WriteLine("Hlavní vlákno čeká..."); t->Wait(); // ekvivalent Join() Console::WriteLine("Task dokončen."); return 0; }
6.2 Task s návratovou hodnotou
Task<int>^ t = Task<int>::Run( gcnew Func<int>([]() -> int { Thread::Sleep(500); return 42; }) ); int vysledek = t->Result; // blokuje, dokud task neskončí Console::WriteLine("Výsledek: {0}", vysledek);
6.3 Spuštění více tasků najednou
array<Task^>^ ukoly = gcnew array<Task^>(3); for (int i = 0; i < 3; i++) { int idx = i; // capture lokální kopie! ukoly[i] = Task::Run(gcnew Action([idx]() { Console::WriteLine("Úkol {0} startuje", idx); Thread::Sleep((1 + idx) * 200); Console::WriteLine("Úkol {0} končí", idx); })); } // Počkáme na VŠECHNY Task::WaitAll(ukoly); // NEBO počkáme na PRVNÍ hotový // int hotovy = Task::WaitAny(ukoly);
6.4 Parallel::For a Parallel::ForEach
using namespace System::Threading::Tasks; // Paralelní for smyčka – automaticky rozdělí iterace mezi vlákna Parallel::For(0, 8, gcnew Action<int>([](int i) { Console::WriteLine("Iterace {0} na vlákně #{1}", i, Thread::CurrentThread->ManagedThreadId); })); // Paralelní ForEach auto data = gcnew array<String^> { "alfa", "beta", "gama" }; Parallel::ForEach(data, gcnew Action<String^>([](String^ s) { Console::WriteLine("Zpracovávám: {0}", s); }));
7. BackgroundWorker
BackgroundWorker je starší ale stále populární třída pro WinForms aplikace – nabízí jednoduché hlášení průběhu a zrušení bez nutnosti řešit marshal volání na UI vlákno ručně.
using namespace System::ComponentModel; BackgroundWorker^ worker = gcnew BackgroundWorker(); worker->WorkerReportsProgress = true; worker->WorkerSupportsCancellation = true; // Událost: vlastní práce na pozadí worker->DoWork += gcnew DoWorkEventHandler( [](Object^ sender, DoWorkEventArgs^ e) { BackgroundWorker^ bw = safe_cast<BackgroundWorker^>(sender); for (int i = 0; i <= 100; i += 10) { if (bw->CancellationPending) { e->Cancel = true; return; } bw->ReportProgress(i); Thread::Sleep(200); } e->Result = "Hotovo!"; }); // Událost: hlášení průběhu (volá se na UI vlákně!) worker->ProgressChanged += gcnew ProgressChangedEventHandler( [](Object^, ProgressChangedEventArgs^ e) { Console::WriteLine("Průběh: {0}%", e->ProgressPercentage); }); // Událost: dokončení worker->RunWorkerCompleted += gcnew RunWorkerCompletedEventHandler( [](Object^, RunWorkerCompletedEventArgs^ e) { if (e->Cancelled) Console::WriteLine("Zrušeno."); else if (e->Error) Console::WriteLine("Chyba: {0}", e->Error->Message); else Console::WriteLine(e->Result); }); worker->RunWorkerAsync(); // spuštění // worker->CancelAsync(); // zrušení
8. Race Condition – závodní podmínka
Race condition nastane, když dvě nebo více vláken přistupují ke sdíleným datům bez synchronizace a alespoň jedno z nich data modifikuje. Výsledek závisí na nepředvídatelném pořadí vláken.
8.1 Ukázka problému
static int pocitadlo = 0; // sdílená proměnná void Inkrementuj() { for (int i = 0; i < 100000; i++) pocitadlo++; // NENÍ atomické! = čti, přičti, zapiš } int main() { Thread^ t1 = gcnew Thread(gcnew ThreadStart(&Inkrementuj)); Thread^ t2 = gcnew Thread(gcnew ThreadStart(&Inkrementuj)); t1->Start(); t2->Start(); t1->Join(); t2->Join(); // Očekáváme 200000, ale dostaneme méně! Console::WriteLine("Výsledek: {0}", pocitadlo); }
9. lock a Monitor
Klíčové slovo lock (v C++/CLI jako Monitor::Enter/Monitor::Exit) zajistí, že do kritické sekce vstoupí vždy jen jedno vlákno.
9.1 Syntaxe v C++/CLI
// C++/CLI nemá klíčové slovo "lock" jako C#, // ale můžeme použít pomocnou třídu msclr::lock #include <msclr/lock.h> ref class BezpecnePocitadlo { private: int _hodnota; Object^ _zamek; // objekt jako zámek (klíč) public: BezpecnePocitadlo() : _hodnota(0) { _zamek = gcnew Object(); } void Inkrementuj() { msclr::lock l(_zamek); // uzamkne a odemkne automaticky (RAII) _hodnota++; } int ZiskatHodnotu() { msclr::lock l(_zamek); return _hodnota; } };
9.2 Monitor::Enter / Exit manuálně
Object^ zamek = gcnew Object(); Monitor::Enter(zamek); try { // kritická sekce – jen jedno vlákno sem smí najednou pocitadlo++; } finally { Monitor::Exit(zamek); // VŽDY odemknout, i při výjimce! }
try/finally, nebo použijte msclr::lock, které zaručí odemčení i při výjimce (RAII princip).
9.3 Monitor::Wait a Pulse – komunikace vláken
Object^ zamek = gcnew Object(); static bool dataPripravena = false; static int data = 0; void Producent() { Monitor::Enter(zamek); data = 99; dataPripravena = true; Monitor::Pulse(zamek); // probudí čekající vlákno Monitor::Exit(zamek); } void Konzument() { Monitor::Enter(zamek); while (!dataPripravena) Monitor::Wait(zamek); // uvolní zámek a čeká na Pulse Console::WriteLine("Přijato: {0}", data); Monitor::Exit(zamek); }
10. Mutex
Mutex (Mutual Exclusion) funguje podobně jako Monitor, ale může být pojmenovaný a sdílený mezi procesy. Hodí se pro zajištění jediné instance aplikace.
// Nepojmenovaný Mutex (jen v rámci procesu) Mutex^ mutex = gcnew Mutex(); mutex->WaitOne(); // uzamkni (blokuje, pokud je uzamčeno jiným vláknem) try { // chráněný kód } finally { mutex->ReleaseMutex(); // odemkni } // Pojmenovaný Mutex – sdílený mezi procesy (jednoinstaness app) bool novyMutex; Mutex^ appMutex = gcnew Mutex(true, "MojeAplikace_Mutex", novyMutex); if (!novyMutex) { Console::WriteLine("Aplikace již běží!"); return 1; }
| Monitor / lock | Mutex | |
|---|---|---|
| Rychlost | Rychlý (jen managed) | Pomalejší (kernel objekt) |
| Meziprocesové | Ne | Ano (pojmenovaný) |
| Timeout | Monitor::TryEnter(ms) | WaitOne(ms) |
| Rekurzivní | Ano | Ano |
11. Semaphore – omezený přístup
Semaphore omezuje počet vláken, která smí být zároveň v kritické sekci. Hodí se například pro omezení počtu souběžných databázových spojení.
// Maximálně 3 vlákna najednou, startujeme s 3 volnými místy SemaphoreSlim^ sem = gcnew SemaphoreSlim(3, 3); void PripojSeDatabaze(Object^ id) { Console::WriteLine("Vlákno {0}: čeká na povolení", id); sem->Wait(); // obsadí jedno místo (nebo čeká) try { Console::WriteLine("Vlákno {0}: PRACUJE", id); Thread::Sleep(1000); // simulace práce s DB } finally { sem->Release(); // uvolní místo Console::WriteLine("Vlákno {0}: hotovo", id); } } int main() { array<Thread^>^ threads = gcnew array<Thread^>(8); for (int i = 0; i < 8; i++) { threads[i] = gcnew Thread(gcnew ParameterizedThreadStart(&PripojSeDatabaze)); threads[i]->Start(i); } for each (auto t in threads) t->Join(); }
12. AutoResetEvent / ManualResetEvent
Event objekty slouží k signalizaci mezi vlákny – jedno vlákno čeká, druhé ho probudí.
// false = zpočátku není nastaveno (vlákno bude čekat) AutoResetEvent^ signal = gcnew AutoResetEvent(false); void Cekajici() { Console::WriteLine("Čekám na signál..."); signal->WaitOne(); // blokuje Console::WriteLine("Signál přijat, pokračuji!"); } int main() { Thread^ t = gcnew Thread(gcnew ThreadStart(&Cekajici)); t->Start(); Thread::Sleep(2000); // hlavní vlákno pracuje Console::WriteLine("Posílám signál!"); signal->Set(); // probudí Cekajici() t->Join(); }
13. Interlocked – atomické operace
Pro jednoduché operace s čísly (increment, decrement, add, exchange) je Interlocked nejrychlejší řešení – nevyžaduje žádný zámek, operace jsou garantovaně atomické na úrovni CPU.
static int pocitadlo = 0; void BezpecnaInkrementace() { for (int i = 0; i < 100000; i++) Interlocked::Increment(pocitadlo); // atomické ++ bez zámku } int main() { Thread^ t1 = gcnew Thread(gcnew ThreadStart(&BezpecnaInkrementace)); Thread^ t2 = gcnew Thread(gcnew ThreadStart(&BezpecnaInkrementace)); t1->Start(); t2->Start(); t1->Join(); t2->Join(); Console::WriteLine("Výsledek: {0}", pocitadlo); // vždy 200000! } // Další metody Interlocked: Interlocked::Decrement(pocitadlo); Interlocked::Add(pocitadlo, 5); int stara = Interlocked::Exchange(pocitadlo, 0); // atomicky nastav na 0, vrať starou Interlocked::CompareExchange(pocitadlo, 99, 0); // if(==0) nastav 99
14. CancellationToken – kooperativní zrušení
Vlákno/Task nelze bezpečně "zabít" zvenčí. Správný způsob je kooperativní zrušení: požádáme vlákno, aby samo skončilo.
using namespace System::Threading; void DlouhaOperace(CancellationToken token) { for (int i = 0; i < 100; i++) { // Zkontrolujeme, zda bylo požádáno o zrušení if (token.IsCancellationRequested) { Console::WriteLine("Operace zrušena po {0} iteracích.", i); return; } Console::WriteLine("Iterace {0}", i); Thread::Sleep(200); } } int main() { CancellationTokenSource^ cts = gcnew CancellationTokenSource(); CancellationToken token = cts->Token; Task^ t = Task::Run(gcnew Action([token]() { DlouhaOperace(token); })); Thread::Sleep(1000); // po 1 sekundě zrušíme cts->Cancel(); t->Wait(); Console::WriteLine("Program skončil."); }
token.ThrowIfCancellationRequested() – vyhodí OperationCanceledException, což je elegantnější pro Tasks s vnořenými voláními.
15. Thread-safe kolekce (Concurrent)
Standardní kolekce (List^, Dictionary^) nejsou thread-safe. Pro práci z více vláken použijte System::Collections::Concurrent:
#using <System.Collections.Concurrent.dll> using namespace System::Collections::Concurrent; // ConcurrentQueue – fronta (FIFO) pro producent-konzument ConcurrentQueue<int>^ fronta = gcnew ConcurrentQueue<int>(); fronta->Enqueue(42); // přidej int hodnota; if (fronta->TryDequeue(hodnota)) Console::WriteLine("Vybráno: {0}", hodnota); // ConcurrentDictionary – slovník pro čtení i zápis z více vláken ConcurrentDictionary<String^, int>^ slovnik = gcnew ConcurrentDictionary<String^, int>(); slovnik->TryAdd("klíč", 1); slovnik->AddOrUpdate("klíč", 1, [](String^, int stara) { return stara + 1; }); // BlockingCollection – blokující fronta pro producent-konzument BlockingCollection<int>^ bc = gcnew BlockingCollection<int>(100); // kapacita 100 bc->Add(1); // blokuje, pokud je plná int item = bc->Take(); // blokuje, pokud je prázdná bc->CompleteAdding(); // signalizuje, že nepřijde nic dalšího
16. Deadlock – uváznutí
Deadlock nastane, když dvě nebo více vláken čekají navzájem na sebe a žádné nemůže pokračovat.
Object^ zamek1 = gcnew Object(); Object^ zamek2 = gcnew Object(); void VlaknoA() { Monitor::Enter(zamek1); Thread::Sleep(100); Monitor::Enter(zamek2); // čeká na B! Monitor::Exit(zamek2); Monitor::Exit(zamek1); } void VlaknoB() { Monitor::Enter(zamek2); Thread::Sleep(100); Monitor::Enter(zamek1); // čeká na A! Monitor::Exit(zamek1); Monitor::Exit(zamek2); }
Prevence deadlocku
- Konstantní pořadí zámků – vždy uzamykejte zámky ve stejném pořadí (nejprve zamek1, pak zamek2).
- TryEnter s timeoutem – pokud se nepodaří uzamknout do X ms, uvolněte co máte a zkuste znovu.
- Minimalizujte zamykání – co nejkratší kritické sekce, co nejméně zámků najednou.
- Jeden velký zámek – jednodušší, ale pomalejší alternativa k mnoha malým zámkům.
if (Monitor::TryEnter(zamek1, 500)) // max 500 ms { try { // kritická sekce } finally { Monitor::Exit(zamek1); } } else { Console::WriteLine("Nepodařilo se uzamknout – zkusím znovu."); }
17. WinForms + UI vlákno
WinForms ovládací prvky (TextBox, Label, ProgressBar…) smí být modifikovány pouze z vlákna, které je vytvořilo (UI vlákno). Přístup z jiného vlákna způsobí výjimku nebo nedefinované chování.
InvalidOperationException: Cross-thread operation not valid.
17.1 Control::Invoke a BeginInvoke
// V background vlákně: void AktualizujUI(String^ zprava) { // Jsme na správném vlákně? if (labelStatus->InvokeRequired) { // Marshalling na UI vlákno labelStatus->Invoke( gcnew Action<String^>([this](String^ s) { labelStatus->Text = s; }), zprava ); } else { labelStatus->Text = zprava; } } // BeginInvoke – asynchronní (neblokuje background vlákno) labelStatus->BeginInvoke( gcnew Action([this]() { progressBar1->Value = 50; }) );
17.2 Progress<T> – moderní přístup
// Vytvoříme na UI vlákně – automaticky marshaluje reporty na UI vlákno Progress<int>^ progress = gcnew Progress<int>( gcnew Action<int>([this](int pct) { progressBar1->Value = pct; // bezpečný přístup k UI labelStatus->Text = pct + "%"; }) ); Task::Run(gcnew Action([progress]() { for (int i = 0; i <= 100; i += 10) { Thread::Sleep(300); (safe_cast<IProgress<int>^>(progress))->Report(i); } }));
18. Kompletní příklad: Producent–Konzument
Klasický vzor: jedno vlákno generuje data (producent), druhé je zpracovává (konzument). Propojuje několik konceptů najednou.
#using <mscorlib.dll> #using <System.Collections.Concurrent.dll> using namespace System; using namespace System::Threading; using namespace System::Threading::Tasks; using namespace System::Collections::Concurrent; ref class ProducentKonzumentDemo { private: BlockingCollection<int>^ _fronta; CancellationTokenSource^ _cts; public: ProducentKonzumentDemo() { _fronta = gcnew BlockingCollection<int>(10); // max 10 v bufferu _cts = gcnew CancellationTokenSource(); } void Producent() { try { for (int i = 1; i <= 20; i++) { _fronta->Add(i, _cts->Token); // blokuje při plné frontě Console::WriteLine("[P] Vyrobeno: {0}", i); Thread::Sleep(100); } } catch (OperationCanceledException^) { } finally { _fronta->CompleteAdding(); // signál: konec produkce Console::WriteLine("[P] Producent skončil."); } } void Konzument() { // GetConsumingEnumerable() blokuje, dokud nejsou data nebo CompleteAdding() for each (int polozka in _fronta->GetConsumingEnumerable(_cts->Token)) { Console::WriteLine(" [K] Zpracováno: {0}", polozka); Thread::Sleep(250); // konzument je pomalejší } Console::WriteLine(" [K] Konzument skončil."); } void Spust() { Task^ tp = Task::Run(gcnew Action(this, &ProducentKonzumentDemo::Producent)); Task^ tk = Task::Run(gcnew Action(this, &ProducentKonzumentDemo::Konzument)); Task::WaitAll(gcnew array<Task^> { tp, tk }); Console::WriteLine("=== Demo dokončeno ==="); } }; int main() { ProducentKonzumentDemo^ demo = gcnew ProducentKonzumentDemo(); demo->Spust(); return 0; }
[P] Vyrobeno: 1 [P] Vyrobeno: 2 [K] Zpracováno: 1 [P] Vyrobeno: 3 [P] Vyrobeno: 4 [K] Zpracováno: 2 ... [P] Producent skončil. [K] Zpracováno: 19 [K] Zpracováno: 20 [K] Konzument skončil. === Demo dokončeno ===
Shrnutí – kdy použít co?
| Potřebuji... | Použiji |
|---|---|
| Jedno vlákno s plnou kontrolou | Thread |
| Krátké úkoly na pozadí | ThreadPool nebo Task |
| Výsledek z vlákna | Task<T> |
| Hlášení průběhu do WinForms | BackgroundWorker nebo Progress<T> |
| Chránit sdílenou proměnnou | Monitor / msclr::lock |
| Atomické čítače | Interlocked |
| Omezit souběžnost | SemaphoreSlim |
| Signalizace mezi vlákny | AutoResetEvent |
| Zrušení operace | CancellationToken |
| Thread-safe kolekce | ConcurrentQueue/Dictionary, BlockingCollection |
- Sdílená data vždy chraňte zámkem nebo Interlocked.
- Zámky uzamykejte vždy ve stejném pořadí.
- Kritické sekce dělejte co nejkratší.
- Používejte
try/finally(nebo RAII lock) – zámek musí být vždy uvolněn. - Vlákna nerušte násilím (
Thread::Abortje deprecated) – používejteCancellationToken. - K UI prvkům přistupujte jen z UI vlákna.