Vlákna v C# - 10. díl
 x   TIP: Přetáhni ikonu na hlavní panel pro připnutí webu
Reklama

Vlákna v C# - 10. dílVlákna v C# - 10. díl

 

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

Google       Google       25. 11. 2008       31 856×

Obsahem tohoto dílu bude zběžné porovnání dostupných timerů v C# s hlavním zaměřením na ten ze třídy System.Threading. Dále se podíváme na Local Storage, klíčové slovo volatile a na atomicitu.

Reklama
Reklama

Nejjednodušším způsobem, jak zavolat nějakou metodu periodicky (po pravidelně se opakujících intervalech), je použití časovačů (timerů). V .NET frameworku je jich hned několik. Když už bereme vlákna, podíváme se nejdříve na ten ze System.Threading.Timer. Třída Timer je velice jednoduchá – obsahuje jen konstruktor a dvě metody (jaká úleva, není toho tolik k popisování ani k pamatování).

Definice této třídy vypadá následovně:

public sealed class Timer : MarshalByRefObject, IDisposable
{
    public Timer (TimerCallback tick, object state, prvniTick, interval);
    public bool Change (prvniTick, interval);   // změna intervalu
    public void Dispose();
}
    //PrvniTick - čas, jak dlouho má Timer
    // udělat první tick
    //interval - intervaly mezi dalšími ticky
    // použijte Timeout.Infinite, pokud chcete 
    // jen jeden tick

V následujícím příkladu, jakmile zapnete program, se spustí odpočítávání 5 vteřin, pak se vypíše na obrazovku nápis „tik ťak“, který se bude opakovat každou vteřinu, dokud uživatel nestiskne Enter.

class Program
{
    static void Main()
    {
        using(new Timer(Tick, "tik ťak", 5000, 1000))
        {
            Console.ReadLine();
        }
    }

    static void Tick(object data)
    {
        // Spustí se na vlákně ve fondu vláken
        Console.WriteLine(data);
    }
}

Jak už jsem řekl, v .NETu existují i jiné timery. Teď se tedy podíváme na namespace System.Timers. Třída Timer z tohoto namespace obaluje tu ze System.Threading, přidává nějakou funkcionalitu navíc a pár změn:

  • Je to komponenta, takže ho můžeme přetáhnout z Toolboxu a pracovat s ním ve Visual Studio Designeru.
  • Má vlastnost Interval namísto metody Change.
  • Má událost Elapsed namísto callback delegátu.
  • Vlastnost Enabled(bool) pro spuštění a zastavení časovače (výchozí je false).
  • Metody Start a Stop (pro případ, že by někomu nevyhovovala vlastnost Enabled).
  • Vlastnost AutoReset pro indikaci, jestli se má časovač spouštět znovu (výchozí je true).

Většinu těchto změn ukazuje následující kód:

using System;
using System.Timers;

class SystemTimer
{
    static void Main()
    {
        var tmr = new Timer();  // Bez argumentů
        tmr.Interval = 500;
        tmr.Elapsed += tmr_Elapsed; // Událost místo callback delegátu
        tmr.Start();    // Spustí timer
        Console.ReadLine();
        tmr.Stop(); // Pozastaví timer
        Console.ReadLine();
        tmr.Start();    // Spustí timer (od předchozí hodnoty)
        Console.ReadLine();
        tmr.Dispose();  // Permanentně stopne timer
    }

    static void tmr_Elapsed(object sender, EventArgs e)
    {
        Console.WriteLine("tik ťak");
    }
}

Existuje ještě třetí timer, který se pro změnu nachází v namespace System.Windows.Forms. Radikálně se liší od timerů z Threading a Timers, protože nepoužívá thread pool, ale vždy vypaluje událost Tick na stejném vlákně, jako byl vytvořen. Pokud ho tedy vytvoříme na primárním vlákně, může přistupovat k ovládacímu prvku a měnit jej v závislosti na „tikání“ bez použití Control.Invoke. Další vlastnosti má společné s timerem ze System.Timers.

WPF má ekvivalentní timer k tomu u WinForms, jen má jiný název – DispatcherTimer.

Local Storage

Dalším tématem (nesouvisejícím s timery), na které se dnes podíváme, je Local Storage neboli „lokální úložiště“. S tímto pojmem jste se mohli setkat například u ASP.NET, Silverlightu a podobných technologií. Vypadá to tak, že každé vlákno dostane přidělené datové úložiště izolované od ostatních vláken. Je užitečné pro ukládání různých informací o zabezpečení, protokolů, ale dá se použít i na jiná data. Kdybychom taková data předávali jako parametry metodám (pokud chceme, aby celé vlákno mělo k těmto datům přístup), bylo by to nepohodlné a měly by k nim přístup jen naše vlastní metody.

Dvě nejdůležitější metody jsou Thread.GetData, která umí číst data z Local Storage, a ThreadSetData, která naopak zapisuje. Obě metody potřebují ke své funkčnosti instanci třídy LocalDataStoreSlot, jež reprezentuje slot (představíme-li si Local Storage jako skříňku, pak je slot něco jako přihrádka - takových přihrádek můžeme mít tolik, kolik chceme). Konstruktor LocalDataStoreSlotu přijímá parametr typu string, který reprezentuje název slotu. Na více vláknech mohou mít sloty stejný název a přitom svoje data nesdílí. Je to kvůli tomu, že každé vlákno má svůj vlastní Local Storage. Příklad využití LS:

class Test
{
    // Stejný objekt LocalDataStoreSlot 
    //můžeme použít napříč všemi vlákny
    LocalDataStoreSlot secSlot = Thread.GetNamedDataSlot("securityLevel");

    // Tato vlastnost bude mít na každém vlákně
    // jinou hodnotu
    int SecurityLevel
    {
        get
        {
            object data = Thread.GetData(secSlot);
            return data == null ? 0 : (int)data;
        }
        set
        {
            Thread.SetData(secSlot, value);
        }
    }

    // Další kód...
}

Metoda Thread.FreeNamedDataSlot zruší slot daného jména na všech vláknech, ale jen tehdy, pokud se už zadané sloty nepoužívají a byly sklizeny garbage collectorem.

To bylo k problematice Local Storage vše, v dnešní poslední kapitole se podíváme na klíčové slovo volatile a na atomicitu.

Neblokující konstrukce

Na úplném začátku druhého dílu byla tabulka nejrůznějších metod synchronizace a na jejím konci byly konstrukce volatile a Interlocked. Jak už víte, synchronizace můžeme dosáhnout pomocí zamykání, ale to patří mezi blokující konstrukce – vlákno čeká, dokud není zámek otevřený. Naštěstí máme k dispozici neblokující konstrukce, které jsou vhodné pro velmi jednoduché a rychlé operace (tzv. atomické, vysvětleno níže), nedochází totiž k žádnému čekání ani blokování.

Atomicita a Interlocked

Slovo atomicita vám může připomínat jistě známější slovíčko atom; není to náhoda, obě jsou z řeckého slova atomos, tedy nedělitelný. Atomická operace se skládá jen z jedné nedělitelné operace (např. sečtení proměnných není atomická operace, protože se musí hodnoty načíst a pak teprve sečíst – sčítání je dělitelná operace). Atomickou operací je například u 32-bitových procesorů přiřazení čísla do proměnné typu int, která je 32-bitová.

class Atomicity
{
    static int x, y;
    static long z;

    static void Test()
    {
        long myLocal;
        x = 3;             // Atomické
        z = 3;             // Neatomické (z je 64-bit)
        myLocal = z;       // Neatomické (z je 64-bit)
        y += x;            // Neatomické (čtení a zapisování)
        x++;               // Neatomické (čtení a zapisování)
    }
}

Práce s 64-bitovými čísly na 32-bitových procesorech není atomická operace, protože vyžaduje alokování dvou 32-bitových míst v paměti. Pokud nějaké vlákno A načítá 64-bitové číslo, zatímco vlákno B ho upravuje, může vlákno A dostat jakýsi mix obou hodnot (protože jedno 64-bitové číslo je složené vlastně ze dvou). Z tohoto odstavce je tedy jasné, proč práce s takovými čísly není atomická.

Atomické nejsou ani unární operátory (takové, které pracují s jednou proměnnou) typu x++. Nejdřív se musí aktuální hodnota „x“ načíst, pak přičíst jedničku a nakonec uložit novou hodnotu. Představte si takovouhle třídu:

class ThreadUnsafe
{
    static int x = 1000;
    static void Go() { for (int i = 0; i < 100; i++) x--; }
}

Možná byste čekali, že pokud metodu Go zavolá deset vláken najednou, proměnná x bude mít hodnotu 0 (cyklus proběhne 100x na deseti vláknech). To nám ale nikdo nezaručí, protože je možné, že jedno vlákno přistoupí k proměnné, zatímco druhé bude získávat její hodnotu, snižovat ji a zapisovat zpátky.

Jedním ze způsobů, jak toto nebezpečí ošetřit, je obalit uvedenou (neatomickou) operaci do locku. Nikdy jsme si to neřekli, ale teď vám to možná došlo – locking vlastně udělá z obalené operace atomickou. Existuje ale druhý, výhodnější způsob. Ten provedeme pomocí třídy Interlocked, která je jednodušší a rychlejší, pokud ji použijeme pro jednoduché operace.

class Program
{
    static long sum;

    static void Main()
    {
        // Inkrementace/dekrementace:
        Interlocked.Increment(ref sum);     // to samé jako: sum++
        Interlocked.Decrement(ref sum);     // sum--

        // Přičtení/odečtení čísla:
        Interlocked.Add(ref sum, 3);        // sum += 3
        Interlocked.Add(ref sum, -2);       // sum -= 2

        // Přečtení hodnoty 64-bit čísla
        Console.WriteLine(Interlocked.Read(ref sum));   // sum == 1

        // Přečte hodnotu a pak zapíše novou
        // Následující řádek napíše "1" a pak změní hodnotu
        // sum na 10
        Console.WriteLine(Interlocked.Exchange(ref sum, 10));   // sum == 10

        // Změní hodnotu proměnné, ale jen pokud se
        // momentálně rovná zadané hodnotě (10)
        Interlocked.CompareExchange(ref sum, 123, 10);  // sum == 123

        // Finální hodnota proměnné sum:
        Console.WriteLine(Interlocked.Read(ref sum));
        
        Console.ReadKey();
    }
}

Používání třídy Interlocked je výhodné, protože obsahuje už předpřipravené atomické metody pro hojně používané operace. Zároveň nemůže dojít k jejímu zablokování, takže nemusíme nést následky přerušení práce.

Memory barriers a volatilita

Vezměme si tento kód:

class Unsafe
{
    static bool konec, boolean;

    static void Main()
    {
        new Thread(Wait).Start();
        Thread.Sleep(1000);
        boolean = true;
        konec = true;
        Console.WriteLine("Něco se děje...");
    }

    static void Wait()
    {
        while (!konec) ;
        Console.WriteLine("A je klid, " + boolean);
        Console.ReadKey();
    }
}

Prohlédněte si jej. Nedělá nic komplikovaného: máme metodu, která se zavolá a je uzavřená v cyklu. Po jedné vteřině ji z toho cyklu osvobodíme nastavením proměnné konec na true a metoda pak vypíše: „A je klid,“ společně s hodnotou proměnné boolean.

Teď si položme otázky: je možné, aby metoda Wait byla pořád uzavřená ve while cyklu i po tom, co se proměnná konec nastaví na true? A je vůbec možné, aby metoda Wait napsala: „A je klid, False“?

Vypadá to nepravděpodobně, že? Ale odpověď na obě otázky je ano. Na víceprocesorových strojích, jakmile se každé vlákno přidělí na jiný procesor, se může stát, že se obě proměnné konec a boolean uloží do cache (vyrovnávací paměti), aby se k nim umožnil rychlejší přístup. Hrozí ale prodleva mezi tím, než se zapíší zpátky do paměti, a nemusí se nutně zapsat ve stejném pořadí, jako se uložily.

Toto riziko můžeme obejít použitím statických metod Thread.VolatileRead a Thread.VolatileWrite při práci s proměnnými. VolatileRead vlastně znamená „přečti poslední hodnotu“ a VolatileWrite zase „zapiš okamžitě do paměti“. Stejného výsledku dosáhneme i elegantněji - deklarováním proměnné jako volatile (v překladu „nestálý“, stejně jako paměť RAM):

volatile static bool konec, boolean;

Pokud proměnnou deklarujeme takto, říkáme tím vlastně: „nekešuj tuhle proměnnou“.

Stejného výsledku bychom dosáhli i použitím prostého locku. Fungovalo by to, protože vedlejším efektem zamykání je vytvoření tzv. „memory barrier“ – máme jistotu, že při vstupu do locku bude mít proměnná svojí nejaktuálnější hodnotu a před opuštěním locku se poslední hodnota zapíše do paměti.

Použít tento postup by bylo nutné v případě, že bychom potřebovali přistupovat k proměnným konec a boolean atomicky, například takhle:

lock (locker) { if (konec) boolean = true; }

Volatilita se týká jen primitivních typů, jiné typy se necachují a nemůžou být ani deklarovány s klíčovým slovem volatile.

To je pro dnešek vše. V následujících dvou dílech probereme zbývající synchronizační konstrukce, jimiž jsou Wait a Pulse.

Zdroj: http://www.albahari.com/threading/part3.aspx#_Timers

×Odeslání článku na tvůj Kindle

Zadej svůj Kindle e-mail a my ti pošleme článek na tvůj Kindle.
Musíš mít povolený příjem obsahu do svého Kindle z naší e-mailové adresy kindle@programujte.com.

E-mailová adresa (např. novak@kindle.com):

TIP: Pokud chceš dostávat naše články každé ráno do svého Kindle, koukni do sekce Články do Kindle.

Hlasování bylo ukončeno    
0 hlasů
Google
(fotka) Jakub KottnauerJakub studuje informatiku na FIT ČVUT, jeho oblíbenou platformou je .NET.
Web     Twitter     Facebook     LinkedIn    

Nové články

Obrázek ke článku Hackerský kongres přiveze v září do Prahy špičky světové kryptoanarchie

Hackerský kongres přiveze v září do Prahy špičky světové kryptoanarchie

Hackerský kongres HCPP16 pořádá od 30. září do 2. října nezisková organizace Paralelní Polis již potřetí, a to ve stejnojmenném bitcoinovém prostoru v pražských Holešovicích. Letos přiveze na třídenní konferenci přes 40 většinou zahraničních speakerů – lídrů z oblastí technologií, decentralizované ekonomiky, politických umění a aktivismu. Náměty jejich přednášek budou také hacking, kryptoměny, věda, svoboda nebo kryptoanarchie.

Reklama
Reklama
Obrázek ke článku ICT PRO školení zaměřené nejenom na ICT

ICT PRO školení zaměřené nejenom na ICT

Dovolte, abychom se představili. Jsme zaměstnanci společnosti ICT Pro, profesionálové v oblasti poskytování komplexních ICT služeb. Neboli služeb spojených s informačními a komunikačními technologiemi, které dnes - ve 21. století - tvoří  nedílnou součást běžného provozu všech moderních firem.

loadingtransparent (function() { var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true; po.src = 'https://apis.google.com/js/plusone.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); })();
Hostujeme u Českého hostingu       ISSN 1801-1586       ⇡ Nahoru Webtea.cz logo © 20032016 Programujte.com
Zasadilo a pěstuje Webtea.cz, šéfredaktor Lukáš Churý