V úvodním díle nového seriálu si povíme, co to vlákna vlastně jsou, k čemu je používáme, a jedno, dvě vlákna si vytvoříme. Seriál není určen pro úplné nováčky, ale spíš pro ty zkušenější, samostatnější programátory.
Co jsou to vlákna?
Jazyk C# podporuje paralelní spouštění kódu pomocí tzv. multithreadingu. Přeloženo do normálního jazyka – můžeme spouštět několik částí kódu najednou, každou část na samostatném vlákně. Představte si, že máte aplikaci, která dělá nějaký složitý a dlouhý výpočet, třeba výpočet čísla pí, nebo renderování. Co se stane, pokud takovouhle akci vyvoláte normálně? Aplikace se zasekne, ale jen zdánlivě, ve skutečnosti poběží operace na pozadí, ale zabere pro sebe celé vlákno, takže se aplikace jeví jako zamrzlá – nemůžete kliknout na žádné tlačítko na formuláři. Pokud ale vytvoříte pro výpočet zvláštní vlákno, výpočet bude probíhat na něm a celé jedno vlákno zbude pro zbytek aplikace. Díky tomu pak můžete aplikací ovlivňovat dění na druhém vlákně – pozastavovat výpočet, přidávat nové hodnoty, vykreslovat průběh do nějakého grafu apod.
C# má ve výchozím stavu jedno vlákno, které pro nás vytvoří běhové prostředí CLR, označované jako primární (nebo hlavní) vlákno, na něm běží každá aplikace, ať už je to Hello World nebo třeba e-mailový klient. Pro vytvoření nového vlákna musíme importovat jmenné prostory System a System.Threading. Základní představu, co to vůbec vlákno je, už máme, takže si ukážeme nějaký příklad vytvoření nového vlákna.
Pro použití vláken je nutné importovat namespace System a System.Threading!
class PrvniVlakno
{
static void Main()
{
Thread t = new Thread(NapisY);
t.Start(); // Spustí NapisY na novém vlákně
while (true) Console.Write("x"); // Bude psát znak "x" na PRIMÁRNÍM vlákně
}
static void NapisY()
{
while (true) Console.Write("y"); // Bude psát znak "y" na druhém vlákně
}
}
Teď nějaké vysvětlení výše uvedeného kódu. Primární vlákno vytvoří nové vlákno pojmenované t, na kterém spustí metodu NapišY. Zároveň s tím poběží na primárním vláknu vypisování písmena „x“.
Co by se stalo, kdybychom spustili obě metody na jednom vlákně? Abychom to zjistili, stačí nám jednoduchá úprava kódu.
class PrvniVlakno
{
static void Main()
{
while (true) Console.Write("x");
while (true) Console.Write("y");
}
}
Zkuste tento úryvek zkompilovat, bude se vypisovat jen písmeno „x“. Vypisování písmenka donekonečna je poměrně časově náročná záležitost :-), a proto se vypisování „y“ nikdy nedostane ke slovu.
CLR přiděluje každému vláknu jeho vlastní zásobník paměti, takže vlákna mohou mít své vlastní proměnné. Při zániku vlákna (např. zavoláním metody Dispose()) paměť Garbage Collector uvolní a znovu rozdělí pro ostatní vlákna. Jako další ukázku si definujeme jednu metodu s cyklem for (a tedy i lokální proměnnou) a zavoláme ji ze dvou vláken.
static void Main()
{
new Thread(Pis).Start(); // Zavolá Pis() na novém vlákně
Pis(); // Zavolá Pis() na primárním vlákně
}
static void Pis()
{
// Lokální proměnná "cykly" v cyklu
for (int cykly = 0; cykly < 5; cykly++) Console.Write("?");
}
V obou zásobnících se vytvořila jedna kopie proměnné cykly, a tak se otazníků vytisklo 10.
Vlákna sdílejí data, pokud mají společný odkaz na stejnou instanci objektu, příklad mluví za vše.
class Vlakno
{
bool hotovo;
static void Main()
{
Vlakno tt = new Vlakno(); // Vytvoření společné instance pro obě vlákna
new Thread(tt.Pis).Start();
tt.Pis();
Console.ReadKey();
}
// Pis() je teď instanční metodou
void Pis()
{
if (!hotovo) { hotovo = true; Console.WriteLine("Hotovo"); }
}
}
Protože obě vlákna volají metodu Pis() ze stejné instance třídy Vlakno, sdílejí proměnnou hotovo. Díky tomu se „Hotovo“ napíše jen jednou, ne dvakrát. I když to neplatí tak úplně vždy, pokud kód není „thread safe“, může dojít k zavolání metody naráz oběma vlákny (co to je „thread safety“ probírám o pár odstavců níže).
Další možností, jak sdílet data, jsou statické proměnné, které jsou sdílené i bez vytvoření instance třídy.
class Vlakno
{
static bool hotovo; // Statické členy jsou sdíleny všemi vlákny
static void Main()
{
new Thread(Pis).Start();
Pis();
Console.ReadKey();
}
static void Pis()
{
if (!hotovo) { hotovo = true; Console.WriteLine("Hotovo"); }
}
}
Oba dva postupy demonstrují jednu klíčovou vlastnost (resp. demonstrují nulovou implementaci té vlastnosti v našem kódu) – tzv. thread safety (česky něco jako „vláknová bezpečnost“, zůstaneme raději u originálního názvu) – tu probere v pozdějších kapitolách podrobně, teď jen prozradím, že kousek kódu je „thread safe“, pokud je schopen korektně běžet když je spuštěn více vlákny a hlavně, pokud kód není „thread safe“, existuje malá pravděpodobnost, že k zavolání metody Pis() dojde v obou vláknech naráz, takže se i slovo „Hotovo“ vypíše dvakrát. Pokud v metodě Pis() prohodíme oba příkazy, šance, že se vypíše „Hotovo“ dvakrát, výrazně stoupne – dokonce hodně nad 50 % (můj skromný odhad činí 60-65 %).
static void Pis()
{
if (!hotovo) { Console.WriteLine("Hotovo"); hotovo = true; }
}
Klíčem k nápravě je udělat kód „thread safe“ pomocí tzv. „zámků“ (locks). Funguje to tak, že když jedno vlákno operuje s proměnnou, dočasně ji zamkne. Jakmile ukončí práci s ní, znovu ji odemkne. Pokud chce ve stejnou chvíli použít stejnou proměnnou i jiné vlákno, chvíli počká, než mu uvolní místo předchozí vlákno. Takto ošetřený kód už konečně můžeme s čistým svědomím prohlásit za „thread safe“.
class Vlakno
{
static bool hotovo;
static object zamek = new object();
static void Main()
{
new Thread(Pis).Start();
Pis();
Console.ReadKey();
}
static void Pis()
{
lock (zamek)
{
if (!hotovo) { Console.WriteLine("Hotovo"); hotovo = true; }
}
}
}
Různá dočasná pauzování práce vláken jsou nutnou součástí synchronizace jednotlivých vláken. Takovou pauzu můžeme vyvolat i explicitně, ne jen když vlákno narazí na zámek. Pro uspání vlákna slouží metoda Sleep().
Thread.Sleep (TimeSpan.FromSeconds (30)); // Přeruší práci vlákna na 30 vteřin
Vlákno také může čekat, dokud se neukončí práce jiného vlákna:
Thread t = new Thread (Pis); // Pis() je nějaká statická metoda
t.Start();
t.Join(); // Počkat, dokud se vlákno nedokončí
Za zmínku stojí, že pauznuté vlákno nespotřebovává systémové zdroje.
Jak vlákna fungují
Multithreading je řízený tzv. „thread schedulerem“ (plánovač vláken). Zajišťuje všem vláknům nějaký čas jejich spuštění a u vláken, která čekají nebo jsou uspaná, zajišťuje, že nespotřebovávají žádný procesorový čas.
Na jednojádrovém počítači provádí thread scheduler „time-slicing“ – velkou rychlostí přepíná mezi jednotlivými aktivními vlákny. Pamatujete, jak náš úplně první příklad vypisoval „x“ a „y“ a jednotlivé skupiny nebyly v početně shodných skupinách? Jednou se vypsalo 10 „x“, pak 12 „y“, podruhé to bylo třeba jen 7 „x“ a tak dále. Tyto nerovnoměrnosti jsou dané právě time-slicingem. Ani počítač se netrefí na milisekundu přesně, protože každé vlákno běželo vždy trochu jinou dobu než vlákno druhé. Pro představu jak je time-slicing rychlý – na Windows XP je frekvence přepínání v desetinách milisekund.
Na vícejádrových počítačích (nebo multiprocesorových systémech) funguje multithreading jako mix time-slicingu a čistého běhu (jedno vlákno připadá na jedno jádro). K time-slicingu musí docházet i tak, protože systém musí obsluhovat svá vlastní vlákna, stejně jako vlákna ostatních aplikací.
Vlákna vs. procesy
Všechna vlákna v jedné aplikaci jsou „uzavřena“ ve společném kontejneru označovaným jako proces. Představte si kabel – balík drátů obalených plastem. Ten obalový plast je proces a jednotlivé dráty jsou vlákna. Proces je jednotka operačního systému, ve kterém běží aplikace.
Vlákna se v ledasčem podobají procesům – například, procesy také podléhají time-slicingu vůči ostatním procesům, jen s tím rozdílem, že procesy jsou naprosto izolované jeden od druhého, zatímco vlákna mezi sebou (uvnitř jedné aplikace) sdílejí haldu (heap; jedna z datových struktur). Právě tahle vlastnost dělá vlákny užitečnými – jedno vlákno něco počítá na pozadí a druhé vypisuje výsledky.
Kdy používat vlákna
Budu se opakovat, ale nejčastějším scénám jsou časově náročné operace. Hlavní vlákno ovládá aplikaci, zatímco druhé (pracovní) vlákno zatím vykonává zadanou práci. Na hlavním vlákně nikdy nic složitého nepočítejte, protože ve Windows Forms a WPF aplikacích nemůže aplikace přijímat žádné příkazy z myši ani z klávesnice, pokud je primární vlákno zaměstnané. Navíc systém označí aplikaci „Neodpovídá“ a uživatelé se pak jen bojí, že se aplikace skutečně zasekla.
Další využití najde multithreading u aplikací, které například čekají na odpověď od jiného počítače (databázový server, klient, …). Pokud toto břímě přenecháme pracovnímu vláknu, můžeme implementovat tlačítka jako Cancel, a uživatel je bude moci dokonce použít.
C# aplikace mohou používat multithreading dvěma způsoby. Buď explicitně vytvoříme další vlákna, nebo použijeme některou ze schopností .NET frameworku, které vytvoří další vlákna za nás. Například BackgroundWorker (předpřipravená kostra pro pracovní vlákno), threading timery, Web Services nebo ASP.NET aplikace se takto chovají. Jedno vláknový ASP.NET server by nebyl moc užitečný, vždyť zpracovává tolik věcí a přijímá příkazy ze všech stran.
Kdy vlákna nepoužívat
Používání zbytečně velkého počtu vláken může vést k velmi složitému programu. Samotný počet nijak aplikaci nezkomplikuje, vždyť je to jen pár instancí nějaké třídy. Co ale celou věc komplikuje, jsou jednotlivé interakce mezi vlákny. Skutečně není nic těžkého se v nich ztratit, a co teprve odhalování a opravování následných bugů. Kvůli tomuto používejte vlákna s rozvahou a jen, když jsou skutečně potřeba! Nevýhodou jsou i zvýšené nároky na procesor, které plynou z přepínání vláken.
To bylo pro dnešek vše, snad vám dal článek alespoň základní informace o vláknech a jejich využití, v příštím díle si ukážeme nějaké praktické příklady.