× Aktuálně z oboru

Konference Game Developers Session! [ clanek/2017060600-konference-game-developers-session/ ]
Celá zprávička [ clanek/2017060600-konference-game-developers-session/ ]

Vlákna v C# - 10. 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 ]       25. 11. 2008       34 185×

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.

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

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