Plynule navážeme na minulý díl, ukážeme si, jak předávat data vláknům, zjistíme, že je můžeme pojmenovávat, a naučíme se ošetřovat vzniklé výjimky.
Vytváření a startování vláken
Vlákna jsou vytvářena pomocí konstruktoru třídy Thread předávajícího delegáta ThreadStart – ten označuje metodu, kde by měla začít práce vlákna. Takhle vypadá deklarace delegáta ThreadStart:
public delegate void ThreadStart();
Poté následuje zavolání metody Start() na instanci vlákna, tato akce uvede vlákno do provozu. Funguje až do chvíle, kdy zpracuje všechny příkazy, které jsme mu zadali. Když vše dokončí, Garbage Collector ho odklidí a uvolní paměť.
class Vlakno
{
static void Main()
{
Thread t = new Thread(new ThreadStart(Pis));
t.Start(); // Spustí Pis() na novém vlákně
Pis(); // Zároveň s tím zavolá Pis() i na hlavním vlákně
}
static void Pis()
{
Console.WriteLine("Ahoj!");
}
}
Tento kód vrátí jako výsledek dvě Ahoj!.
Nebylo by to C#, kdyby se nám celou věc nepokusilo trochu zjednodušit. Můžeme celé ThreadStart vypustit, kompilátor si ho tam umí doplnit sám:
static void Main()
{
Thread t = new Thread (Pis);
...
}
static void Pis(){ ... }
Další způsob, jak si ušetřit práci, je použití anonymních metod:
static void Main()
{
Thread t = new Thread(delegate() { Console.WriteLine("Ahoj!"); });
t.Start();
Pis();
}
Vlákna mají vlastnost IsAlive, která vrací true, pokud bylo vlákno už spuštěno (tedy byla zavolána metoda Start()), až do jeho zániku. Vlákno po skončení své činnosti nemůže být restartováno, protože ho, jak už jsem zmínil, Garbage Collector odklidí.
Předávání dat delegátovi ThreadStart
Řekněme, že chcete v příkladu nahoře lépe rozlišit, které „Ahoj!“ napsalo které vlákno, třeba tím, že jedno ze slov napíšeme velkými písmeny. Normálně by šlo by předat nějaký parametr metodě Pis(), ale to nemůžeme, protože delegát ThreadStart nepřijímá žádné argumenty. Naštěstí má .NET framework další verzi delegáta a tou je ParametrizedThreadStart, který přímá argument typu object, tak jako v příkladu:
public delegate void ParameterizedThreadStart (object obj);
Upravený příklad z předchozí kapitoly bude vypadat takto:
class ThreadTest
{
static void Main()
{
Thread t = new Thread(Pis);
t.Start(true); // == Pis(true)
Pis(false);
}
static void Pis(object velkaPismena)
{
bool velka = (bool)velkaPismena;
Console.WriteLine(velka ? "AHOJ!" : "ahoj!");
}
}
Divíte se, kde je nějaký delegát? Kompilátor si ho opět sám dosadí, pokud totiž předáte parametr volané metodě, automaticky se použije ParametrizedThreadStart namísto ThreadStart.
Parametr předávávaný delegátovi ParametrizedThreadStart přijímá právě jeden parametr typu object, při použití ho tedy vždy musíme přetypovat, stejně jako já to udělal s přetypováním na boolean.
Další možností, jak vyřešit příklad nahoře, je opět použití anonymních metod.
static void Main()
{
Thread t = new Thread(delegate() { Pis("Ahoj"); });
t.Start ();
}
static void Pis (string text)
{
Console.WriteLine(text);
}
Výhoda tohoto postupu spočívá v tom, že metoda WriteLine přijímá libovolný počet argumentů a nejsme omezování jako při použití ParametrizedThreadStart.)
Do třetice, další způsob předávání dat je přes instanční metody namísto statických metod. Jednotlivé vlastnosti instance pak říkají vláknu, co má dělat.
class Vlakna
{
bool velka;
static void Main()
{
Vlakna instance1 = new Vlakna();
instance1.velka = true;
Thread t = new Thread(instance1.Pis);
t.Start();
Vlakna instance2 = new Vlakna();
instance2.Pis();
}
void Pis()
{
Console.WriteLine(velká ? "AHOJ!" : "ahoj!");
}
}
Pojmenovávání vláken
Vlákno můžeme pojmenovat přes vlastnost Name. Velmi to usnadňuje debugging (prostě víme, co je které vlákno zač) a s názvy vláken si můžeme hrát i v konzoli.
Jméno vlákna můžeme nastavit kdykoliv se nám zachce, jen musí existovat. Ale pozor, jméno můžeme nastavit jen jednou, jinak dostaneme výjimku!
V následujícím příkladu, protože neběží ve chvíli, kdy upravujeme název vlákna, více než jedno (hlavní) vlákno, můžeme k němu přistoupit přes statickou vlastnost CurrentThread.
class Pojmenovavani
{
static void Main()
{
Thread.CurrentThread.Name = "hlavní";
Thread pracovni = new Thread(Pis);
pracovni.Name = "pracovní";
pracovni.Start();
Pis();
Console.ReadKey();
}
static void Pis()
{
Console.WriteLine("Zdraví vás " + Thread.CurrentThread.Name + " vlákno");
}
}
Vlákna běžící v popředí a pozadí
Ve výchozím stavu běží vlákna na popředí, to znamená, že aplikace funguje tak dlouho, dokud alespoň jedno z nich běží. C# umožňuje využití i vláken běžících na pozadí – pokud vypneme všechna vlákna na popředí, aplikace se vypne, i když nějaká vlákna v pozadí ještě fungují.
Změna vlákna z popředí na pozadí nezmění jeho prioritu vůči ostatním vláknům, ani potřebný procesorový čas.
Vlákna mají vlastnost IsBackground, která, jak jistě tušíte, nastavuje (pokud má hodnotu true) vlákno na vlákno běžící v pozadí.
class VlaknaNaPozadi
{
static void Main(string[] args)
{
Thread pracovniV = new Thread(delegate() { Console.ReadLine(); });
if (args.Length > 0) pracovniV.IsBackground = true;
pracovniV.Start();
}
}
Pokud je program spuštěn bez parametrů, pracovní vlákno je ve výchozím stavu – běží na popředí, a zastaví se na Console.ReadLine(), kde čeká, až uživatel stiskne klávesu Enter. Mezitím hlavní vlákno pořád běží a aplikace funguje, protože hlavní vlákno je aktivní.
Pokud bychom ale metodě Main() předali nějaký parametr, pracovní vlákno by se přepnulo do práce na pozadí a aplikace by se téměř okamžitě ukončila, protože hlavní (které běží v popředí) hned ukončí svoji práci a nebere ohledy na to, že nějaké vlákno na pozadí ještě běží.
Když je vlákno běžící na pozadí ukončeno takhle „násilně“, přeskočí se v něm i všechny případné bloky „finally“. Toto chování je nežádoucí (proč bychom nějaké finally vůbec psali, kdybychom ho chtěli přeskakovat), a proto je dobré navyknout si počkat vždy než vlákna na pozadí ukončí svou práci a do té doby práci vláken v popředí pozastavit, třeba pomocí Thread.Join (viz minulý díl).
Nastavovat pracovní vlákna jako vlákna běžící na pozadí je výhodné v tom, že máme snadnou kontrolu nad vypínáním aplikace. Představme si opak – vlákno v popředí, které samo při vypnutí aplikace (tedy vypnutí hlavního vlákna) nezemře. Taková aplikace sice zmizí ve Správci úloh ze záložky Aplikace, ale pořád bude její proces aktivní na záložce Procesy. Dokud sám uživatel neukončí na záložce Procesy daný proces, bude běžet a spotřebovávat systémové zdroje.
Nejčastějším zdrojem problémů vypínaných aplikací jsou zapomenutá vlákna běžící na popředí!
Priorita vláken
Vlastnost vláken zvaná Priority určuje, kolik dané vlákno dostane času na vykonání své činnosti. Vzpomínáte na minulý díl, kde jsem se zmiňoval, že CLR přepíná mezi vlákny každou přibližně desetinu milisekundy? Právě vlastností Priority se dá tato hodnota mírně upravit.
Tato vlastnost je udělaná jako výčet (typ enum) hodnot Lowest, BelowNormal, Normal, AboveNormal a High (seřazeno od nejnižší priority po nejvyšší). Nastavená hodnota se projeví jen tehdy, pokud je zároveň spuštěno více vláken.
Ošetřování výjimek
Jakékoliv „obecné“ try/catch/finally bloky nemají žádný význam, pokud je nové vlákno spuštěné, běží totiž na jiné úrovni a bloky, jako v příkladu níže, bude ignorovat.
public static void Main()
{
try
{
new Thread(Pis).Start();
}
catch (Exception ex)
{
// Sem se ani nikdy nedostaneme!
Console.WriteLine("Výjimka!");
}
}
static void Pis() { throw null; }
Ke bloku catch ani nedojde, takže ani try by tam nemuselo být. Výsledkem bude nová vlákno s neošetřenou výjimkou NullReferenceException. Řešením je napsání těchto bloků zvlášť pro každou metodu, kterou nové vlákno spouští.
public static void Main()
{
new Thread (Pis).Start();
}
static void Pis()
{
try
{
...
throw null; // tuhle výjimku to už zachytí
...
}
catch (Exception ex)
{
//Nějaké ošetření výjimky…
...
}
}
Od .NET frameworku 2.0 výše, jakákoliv neošetřená výjimka na vlákně shodí celou aplikaci, takže ignorovat je není způsob jak daný problém vyřešit. Bloky try/catch musí být v každé metodě (abychom 100% zamezili pádům), což při větším počtu metod začíná být skutečně nepraktické. Jste Windows Forms programátor a používáte časté „globální“ zachycování výjimek?
static class Program
{
static void Main()
{
Application.ThreadException += Osetreni;
Application.Run (new MainForm());
}
static void Osetreni (object sender, ThreadExceptionEventArgs e)
{
// Zachycení, zapsaní, ošetření výjimky…
}
}
Událost Application.ThreadException se zavolá, když naposled volaný kód (jako odpověď na nějakou Windows zprávu) vytvoří výjimku. Toto řešení sice funguje skvěle, ale dává nám falešný pocit bezpečí. Chyby vytvořené pracovními vlákny totiž ani ThreadException nezachytí. Naštěstí máme k dispozici low-level řešení – AppDomain.UnhandledException. K zavolání dojde kdykoliv, kdy dojde na jakémkoliv vlákně k chybě, v jakémkoliv typu aplikace (ať už s UI nebo bez něj). Ovšem nedoporučuji používat tuto událost jako primární pro zachycení výjimek, použijte ji spíš jako poslední záchranu před pádem aplikace.
A to je konec, příště nás čeká přehled způsobů jak synchronizovat práci vláken.