× Aktuálně z oboru

Programátoři po celém světě dnes slaví Den programátorů [ clanek/2018091300-programatori-po-celem-svete-dnes-slavi-den-programatoru/ ]
Celá zprávička [ clanek/2018091300-programatori-po-celem-svete-dnes-slavi-den-programatoru/ ]

Vlákna v C# - 13. díl

[ http://programujte.com/profil/9617-jakub-kottnauer/ ]Google [ https://plus.google.com/+JakubKottnauer?rel=author ]       [ http://programujte.com/profil/14523-martin-simecek/ ]Google [ ?rel=author ]       24. 12. 2008       28 200×

V posledním díle naší téměř půlroční procházky světem vláken v C# nás čekají metody Suspend, Resume a Abort.

Suspend a Resume

Vlákno může být explicitně „suspendováno“ a znovu uvedeno do chodu pomocí metod Thread.Suspend a Thread.Resume. Tento mechanismus je naprosto oddělený od blokování – oba systémy mohou pracovat paralelně a nezávisle na sobě.

Jedno vlákno může suspendovat samo sebe nebo jiné vlákno. Volání Suspend vyústí ve vstoupení vlákna do stavu SuspendRequested („požadována suspendace“) a jakmile to bude pro garbage collector vhodné, přenastaví se na Suspended. Z tohoto bodu může být vlákno opět uvedeno do provozu jen tím, že jiné vlákno zavolá metodu Resume. Resume funguje jen na suspendovaná vlákna, ne na zablokovaná.

Od dob .NET 2.0 jsou Suspend a Resume málo používané kvůli nebezpečí v některých situacích. Pokud budeme pracovat s nějakými veledůležitými (třeba systémovými) resources a jejich vlákno bude suspendováno, celá aplikace (nebo i počítač) se může dostat do deadlocku.

Ale bezpečné je zavolat Suspend na nějaké vlákno pomocí jednoduchého synchronizačního mechanismu – zavoláme Suspend na vlákno A, pak čekáme, než ho třeba hlavní vlákno probudí. Problém je ale v testování, jestli je vlákno A suspendováno, nebo ne:

static void Main()
{
    worker.NextTask = "MowTheLawn";
    if ((worker.ThreadState & ThreadState.Suspended) > 0)
        worker.Resume;
    else
        // Nemůžeme zavolat Resume, protože vlákno není
        // suspendováno
        // Místo toho přenastavíme proměnnou
        worker.AnotherTaskAwaits = true;
}

Tento postup je příšerně thread-unsafe. Může dojít k jeho narušení kdekoliv mezi pouhými pěti řádky, které jsme si napsali. Přestože by se to dalo všelijak ošetřit, bylo by to mnohem složitější než různé alternativy (jako třeba AutoResetEvent a Monitor.Wait). Tato skutečnost dělá Suspend a Resume naprosto neužitečnými na všech frontách, uvedli jsme si je tu jen pro úplnost.

Metoda Abort

Vlákna můžeme násilně ukončit právě pomocí metody Abort:

class Abort
{
    static void Main()
    {
        Thread t = new Thread(delegate() { while (true);});   // Zacyklení
        t.Start();
        Thread.Sleep(1000); // Necháme to vteřinku běžet...
        t.Abort();  // a pak ukončíme
    }
}

Vlákno, které se chystáme ukončit, přejde okamžitě po zavolání Abort do stavu AbortRequested. Pokud se pak bez problémů ukončí, přejde do stavu Stopped. Na tuto situaci můžeme počkat třeba pomocí Join:

class Abort
{
    static void Main()
    {
        Thread t = new Thread(delegate() { while (true); });
        Console.WriteLine(t.ThreadState);     // Unstarted

        t.Start();
        Thread.Sleep(1000);
        Console.WriteLine(t.ThreadState);     // Running

        t.Abort();
        Console.WriteLine(t.ThreadState);     // AbortRequested

        t.Join();
        Console.WriteLine(t.ThreadState);     // Stopped
    }
}

Po zavolání Abort se na cílovém vlákně vytvoří výjimka ThreadAbortException. Můžeme ji zachytit, ale pak se tato výjimka znovu vytvoří na konci catch bloku (aby se zajistlo, že vlákno skutečně skončí v pořádku). Znovuvyvolání můžeme zabránit zavoláním Thread.ResetAbort někde uvnitř catch bloku, vlákno pak znovu vstoupí do stavu Running, ze kterého ho znovu můžeme pomocí Abort dostat do stavu AbortRequested. V následujícím příkladu vyvoláme vlákno z mrtvých zpět mezi živé vždy, když se pokusíme zavolat Abort:

class Terminator
{
    static void Main()
    {
        Thread t = new Thread(Work);
        t.Start();
        Thread.Sleep(1000); t.Abort();
        Thread.Sleep(1000); t.Abort();
        Thread.Sleep(1000); t.Abort();
    }

    static void Work()
    {
        while (true)
        {
            try { while (true); }
            catch (ThreadAbortException) { Thread.ResetAbort(); }
            Console.WriteLine("Já neumřu!");
        }
    }
}

Výjimka ThreadAbortException má jednu důležitou zvláštnost: pokud ji neošetříme, aplikace nespadne (na rozdíl od všech ostatních výjimek)!

Abort vám bude fungovat na vlákně v téměř jakémkoliv stavu – ať už normálně pracuje, je zablokované, suspendované nebo třeba zastavené. Pokud Abort zavoláme na suspendované vlákno, vznikne výjimka ThreadStateException (název na první pohled podobný ThreadAbortException probrané před chvílí, neplést!) na volajícím vlákně a proces přerušování vlákna pomocí Abort bude pokračovat teprve až nebude suspendováno. Takhle to funguje:

try { suspendedThread.Abort(); }
catch (ThreadStateException) { suspendedThread.Resume(); }
// Teď se suspendThread ukončí

Problémy s Thread.Abort

Možná si myslíte, že pokud vlákno nezavolá ResetAbort, můžeme očekávat, že se po zavolání Abort ukončí poměrně rychle. Ovšem existuje několik faktorů, které mohou držet vlákno ve stavu AbortRequested docela dlouhou dobu:

  • Statické konstruktory nejsou nikdy ukončeny v polovině jejich kódu – vždy se musí dokončit celý kód konstruktoru.
  • Stejně tak nikdy nedojde k utnutí kódu v catch/finally blocích.
  • Pokud je zavoláno Abort a vlákno zrovna spouští unmanaged kód, dojde k ukončení teprve až se vlákno znovu dostane k managed kódu.

Poslední faktor nám může způsobit problémy, protože sám .NET Framework často využívá unmanaged kód a to po dlouhou dobu. Příkladem unmanaged tříd jsou různé třídy pro práci se sítí nebo s databázemi. Pokud máme server s pomalou odezvou, můžeme v oblasti unmanaged kódu zůstat klidně i několik minut (to samozřejmě závisí na samotném serveru, složitosti požadavku, …).

Používání Abort v kombinaci s čistým managed kódem je bez větších problémů, pokud používáme using, anebo ve finally bloku voláme Dispose. I tak jsme ale pořád ohroženi nějakými nepříjemnými překvapeními, podívejte se na tento kód:

using (StreamWriter w = File.CreateText("myfile.txt"))
    w.Write("Abort-Safe?");

Tento zápis pro vás asi není nic nového, kompilátor si ho převede do takovéto podoby:

StreamWriter w = File.CreateText("myfile.txt");
try { w.Write("Abort-Safe"); }
finally { w.Dispose(); }  

A tady je ten problém. Může se totiž stát, že se Abort zavolá ve chvíli, kdy je sice instance StreamWriteru už vytvořená, ale před tím, než stihne aplikace spustit try blok. Pokud bychom se podívali na IL kód, zjistili bychom, že to samé se může stát i zatímco se instance StreamWriteru přiřazuje do proměnné w:

IL_0001:  ldstr      "myfile.txt"
IL_0006:  call       class [mscorlib]System.IO.StreamWriter
                     [mscorlib]System.IO.File::CreateText(string)
IL_000b:  stloc.0
.try
{
  ...

Ať už nastane jakýkoliv problém z předešlého odstavce, Dispose ve finally bloku se obejde a zůstane nám „prázdná“ instance StreamWriteru, která zabrání všem dalším pokusům o vytvoření souboru myfile.txt, dokud neskončí samotná aplikační doména.

Vyvstává tedy otázka, jak vlastně správně napsat „abort-friendly“ metodu. Nejběžnějším způsobem je ten, že vůbec Abort v metodě nezavoláme, místo toho jen nastavíme proměnnou, která indikuje, že by metoda Abort měla být zavolána. Pracovní vlákno pak periodicky kontroluje stav té proměnné a pokud je true, zavolá se Abort. Vše bude ještě lepší, pokud pracovní vlákno zavolá Abort samo na sebe – díky tomu je vlákno „abortnuto“ hned po skončení try/finally bloků:

class ProLife
{
    public static void Main()
    {
        Worker w = new Worker();
        Thread t = new Thread(w.Work);
        t.Start();
        Thread.Sleep(500);
        w.Abort();
    }

    public class Worker
    {
        // volatile zajistí, že proměnná abort nebude cachována
        volatile bool abort;

        public void Abort() { abort = true; }

        public void Work()
        {
            while (true)
            {
                CheckAbort();
                try { OtherMethod(); }
                finally { /* jakýkoliv potřebný úklid */ }
            }
        }

        void OtherMethod()
        {
            // Nějaká práce...
            CheckAbort();
        }

        void CheckAbort() { if (abort) Thread.CurrentThread.Abort(); }
    }
}

Ukončování aplikačních domén

Dalším způsobem, jak zajistit bezpečnost volání Abort, je mít požadované vlákno v jeho vlastní aplikační doméně. Po zavolání Abort se totiž ukončí celá doména, společně se všemi instancemi, resources, … které nebyly správně ukončeny (případ StreamWriteru o několik řádků výše).

Abych pravdu řekl, je volání Abort zbytečné, protože když se ukončí aplikační doména, všechna vlákna se ukončí také (pomocí automatického zavolání Abort). Spoléhat se na to má ale jednu nevýhodu. Pokud bude volání Abort dlouho trvat (třeba kvůli dlouhému kódu v nějakém finally bloku), aplikační doména se neukončí a místo toho se vytvoří výjimka CannotUnloadAppDomainException. Z tohoto důvodu je lepší před ukončením aplikační domény zavolat explicitně Abort a Join s nějakým timeoutem, který si sami specifikujeme.

V následujícím příkladu vstupuje pracovní vlákno do nekonečného cyklu, kde pořád dokola vytváří a zavírá soubor pomocí abort-unsafe metody File.CreateText. Hlavní vlákno opakovaně vytváří a abortuje zmíněná pracovní vlákna. Aplikace většinou spadne po jedné nebo dvou iteracích, kvůli CreateText metodě, kterou přerušíme v půlce její práce:

using System;
using System.IO;
using System.Threading;

class Program
{
    static void Main()
    {
        while (true)
        {
            Thread t = new Thread(Work);
            t.Start();
            Thread.Sleep(100);
            t.Abort();
            Console.WriteLine("Abort");
        }
    }

    static void Work()
    {
        while (true)
            using (StreamWriter w = File.CreateText("myfile.txt")) { }
    }
}

Teď si ukážeme stejný program, jen upravený, aby pracovní vlákno běželo ve své vlastní aplikační doméně, která je zrušena (metodou Unload) po ukončení vlákna. Aplikace poběží bez chyb, protože se „poškozená“ aplikační doména s neplatným odkazem na soubor vždy zruší.

using System;
using System.IO;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        while (true)
        {
            AppDomain ad = AppDomain.CreateDomain("worker");
            // Lambda výraz v kódu je ekvivalentní k:
            // Thread t = new Thread (delegate() { ad.DoCallBack (Work); });
            Thread t = new Thread(() => ad.DoCallBack(Work));
            t.Start();
            Thread.Sleep(100);
            t.Abort();
            if (!t.Join(2000))
            {
                /* Vlákno ještě neskončí i přesto, že jsme zavolali Abort
                 * Zde můžete umístit další kód dle potřeby */
            }
            AppDomain.Unload(ad);   // Zničíme rozbitou doménu
            Console.WriteLine("Aborted");
        }
    }

    static void Work()
    {
        while (true)
            using (StreamWriter w = File.CreateText("myfile.txt")) { }
    }
}

Výstup bude podle všech očekávání: tisknutí textu „Aborted“ pořád dokola.

Vytváření a ničení aplikačních domén je ve světě počítačů považované za poměrně dlouho trvající operaci, trvá totiž i několik milisekund.

Ukončování procesů

Poslední situací, kdy může vlákno přestat existovat, je při ukončení rodičovského procesu. Taková situace nastane, pokud pracovnímu vláknu řekneme, že má běžet na pozadí (Vzpomínáte? Pomocí vlastnosti IsBackground nastavené na true.) a hlavní vlákno dokončí svoji práci. Pracovní vlákno pak není schopné udržet aplikace při životě (je ignorováno, běží přece někde na pozadí), ukončí se proces aplikace a pracovní vlákno se díky tomu ukončí také.

Když je vlákno ukončeno kvůli konci rodičovského procesu, je v tu ránu mrtvé, ani žádné finally bloky se neprovedou. Ke stejné situaci dojde, pokud aplikaci ukončíme přes Správce úloh (Ctrl + Shift + Esc) nebo přes metodu Process.Kill.

Vypadá to, že jsme na konci. Tím myslím úplně. Během dlouhého půl roku jsme se ve čtrnácti dílech (pokud počítám i ten úvodní) podívali na základy i na pokročilá témata ohledně vláken a řekli jsme si o spoustě tříd. Některé z nich sice dnes využití nenajdou, ale je dobré o nich vědět, nemyslíte?

Protože tento díl vyšel na Vánoce, jako dárek jsem všech 14 dílů předělal do PDF, do lépe tisknutelné podoby. Ať slouží. [ http://programujte.com/storage/200812222036_C_threading.pdf ] A mimochodem, v budoucnu snad vyjde 14. díl o novinkách ohledně vláken v .NET 4.0 (na poli threadingu bude několik novinek, hlavně paralelní programování). Snad se vám seriál líbil, obohatil vaše znalosti o nějaké tipy a uvidíme se někdy příště u dalšího seriálu!

Zdroj: http://www.albahari.com/threading/part4.aspx#_Suspending_and_Resuming

Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2008111700-vlakna-v-c-13-dil/ ].