Multithreading v C++/CLI

Podrobný průvodce pro začátečníky – od prvního vlákna po synchronizaci a TPL

C++/CLI .NET Runtime System::Threading

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.

Proces (EXE) ├─ Hlavní vlákno (Thread 0) ← startuje funkce main() / WinMain() ├─ Vlákno 1 ← vytvoříme ručně ├─ Vlákno 2 ← např. pro síťové IO └─ Vlákno N ← každé sdílí paměť procesu

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í.
⚠ Pozor Vlákna sdílí paměť procesu. To je výhoda (bez kopírování dat), ale také hlavní zdroj chyb (race conditions, deadlocky). Celý tento průvodce vás naučí, jak se těmto problémům vyhnout.

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:

TypHlavičkyPouž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.

ℹ Nastavení projektu Projekt musí být typu CLR Empty Project nebo CLR Console Application ve Visual Studiu. Zkontrolujte Vlastnosti → Obecné → Podpora CLR: Podpora CLR (/clr).

3. Přehled System::Threading

Jmenný prostor System::Threading nabízí vše potřebné:

Třída / typCo dělá
ThreadNízkoúrovňové vlákno, plná kontrola
ThreadPoolFond recyklovaných vláken – lehčí na zdroje
TaskVyšší abstrakce (TPL), async/await vzor
MonitorZákladní synchronizační primitiv (lock)
MutexVýhradní přístup, i mezi procesy
SemaphoreOmezený počet souběžných přístupů
AutoResetEventSignalizace mezi vlákny
InterlockedAtomické operace s čísly
CancellationTokenKooperativní zrušení vlákna/tasku

4. Třída Thread – vytvoření prvního vlákna

4.1 Nejjednodušší příklad

C++/CLI
#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;
}
Možný výstup (pořadí se může lišit!)
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.
✔ Klíčový koncept: Join() Metoda 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^:

C++/CLI
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:

C++/CLI
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

C++/CLI
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 Foreground vlákna (výchozí) udrží proces naživu. Background vlákna jsou zabita automaticky, když skončí všechna foreground vlákna. Pro pomocná vlákna nastavujte 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.

C++/CLI
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;
}
⚠ Omezení ThreadPool
  • Nelze nastavit prioritu ani name jednotlivého vlákna.
  • Nelze volat Join() – pro čekání na dokončení použijte CountdownEvent nebo Task.
  • 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

C++/CLI
#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

C++/CLI
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

C++/CLI
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

C++/CLI
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ě.

C++/CLI
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

ŠPATNĚ – race condition!
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);
}
Problém – ztracená aktualizace: Vlákno 1: čti pocitadlo (= 5) ──────────────────────────► zapiš 6 Vlákno 2: čti pocitadlo (= 5) ──► zapiš 6 Oba zapíší 6, ale správná hodnota je 7 – jedno přičtení je ztraceno!

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
// 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ě

C++/CLI
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!
}
✔ Best practice Vždy vložte kritickou sekci do 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

C++/CLI
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.

C++/CLI
// 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 / lockMutex
RychlostRychlý (jen managed)Pomalejší (kernel objekt)
MeziprocesovéNeAno (pojmenovaný)
TimeoutMonitor::TryEnter(ms)WaitOne(ms)
RekurzivníAnoAno

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í.

C++/CLI
// 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í.

AutoResetEvent: Set() probudí JEDNO čekající vlákno a automaticky se resetuje ManualResetEvent: Set() probudí VŠECHNA čekající vlákna; reset musíme volat ručně
C++/CLI
// 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.

C++/CLI
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.

C++/CLI
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.");
}
✔ ThrowIfCancellationRequested() Alternativně volejte 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:

C++/CLI
#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.

Deadlock scénář: Vlákno A: uzamkne zamek1 ──► čeká na zamek2 (drží ho B) Vlákno B: uzamkne zamek2 ──► čeká na zamek1 (drží ho A) Oba čekají navždy ──► program zamrzne
ŠPATNĚ – potenciální deadlock
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.
C++/CLI – TryEnter
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í.

🚫 Chybný přístup Přímý přístup k UI prvku z background vlákna vyvolá InvalidOperationException: Cross-thread operation not valid.

17.1 Control::Invoke a BeginInvoke

C++/CLI – WinForms
// 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

C++/CLI
// 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.

C++/CLI – Producent-Konzument s BlockingCollection
#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;
}
Ukázkový výstup
[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 kontrolouThread
Krátké úkoly na pozadíThreadPool nebo Task
Výsledek z vláknaTask<T>
Hlášení průběhu do WinFormsBackgroundWorker nebo Progress<T>
Chránit sdílenou proměnnouMonitor / msclr::lock
Atomické čítačeInterlocked
Omezit souběžnostSemaphoreSlim
Signalizace mezi vláknyAutoResetEvent
Zrušení operaceCancellationToken
Thread-safe kolekceConcurrentQueue/Dictionary, BlockingCollection
✔ Zlatá pravidla multithreadingu
  • 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::Abort je deprecated) – používejte CancellationToken.
  • K UI prvkům přistupujte jen z UI vlákna.