IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

2. diel - Vlákna v C # .NET - Sleep, Join a lock

V minulej lekcii, Úvod do viacvláknových aplikácií v C # .NET , sme si vytvorili prvú viacvláknové aplikácie. V dnešnom C# .NET tutoriálu sa naučíme vlákna blokovať a zamykať.

Sleep a Join

Aktuálne vlákno môžeme uspať na daný počet milisekúnd a to pomocou statickej metódy Sleep() na triede Thread. Vlákno je blokované kým čas nevyprší, potom sa opäť prebúdza a pokračuje vo svojej činnosti.

Vytvorme si nový projekt s triedou Vypisovac, ktorá bude vyzerať podobne, ako Prepinac z minulej lekcie:

class Vypisovac
{

    public void Vypisuj0()
    {
        for (int i = 0; i < 100; i++)
        {
            Console.Write("0");
            Thread.Sleep(5);
        }
    }

    public void Vypisuj1()
    {
        for (int i = 0; i < 150; i++)
        {
            Console.Write("1");
            Thread.Sleep(5);
        }
    }

}

Metóda Vypisuj0() vypíše do konzoly 100 núl a pri každom výpise uspí svoje vlákno na 5ms. Vypisuj1() vypíše 150 jedničiek a pobeží teda dlhšie (asi o 1/4 sekundy) ako metóda Vypisuj0 ().

Teraz v hlavnej metóde vytvoríme vlákno pre každú metódu a vlákna spustíme. Nakoniec vypíšeme "Hotovo":

Vypisovac vypisovac = new Vypisovac();
Thread vlakno1 = new Thread(vypisovac.Vypisuj0);
Thread vlakno2 = new Thread(vypisovac.Vypisuj1);
vlakno1.Start();
vlakno2.Start();
Console.WriteLine("Hotovo");

Výstup aplikácie je nasledovné:

Join vlákien v C# .NET - Paralelné programovanie a viacvláknové aplikácie v C # .NET

"Hotovo" sa vypísalo ako prvý, pretože hlavné vlákno nečakalo na vypisovací vlákna. Na dokončenie činnosti vlákna môžeme počkať a to pomocou metódy Join(), ktorá zablokuje aktuálne vlákno, kým sa metóda nedokončí. Upravme náš kód do nasledujúcej podoby:

Vypisovac vypisovac = new Vypisovac();
Thread vlakno1 = new Thread(vypisovac.Vypisuj0);
Thread vlakno2 = new Thread(vypisovac.Vypisuj1);
vlakno1.Start();
vlakno2.Start();
vlakno1.Join();
vlakno2.Join();
Console.WriteLine("Hotovo");

Hlavné vlákno teraz čaká až obe vlákna dokončí svoju prácu. Výsledok je nasledujúci:

Join vlákien v C# .NET - Paralelné programovanie a viacvláknové aplikácie v C # .NET

Ak by sme chceli nejaké vlákno uspať na dlhú dobu, môžeme miesto prepočítavanie hodín na sekundy odovzdať v parametri inštancii TimeSpan. Trieda TimeSpan má statické metódy ako FromHours() a podobne:

Thread.Sleep(TimeSpan.FromHours(2));

Ak chceme, aby systém nejaké vlákno prepol, môžeme ho nechať spať aj na 0 ms. Samotné volanie Thread.Sleep() vlákno vždy zablokuje. Podobný efekt môžeme dosiahnuť pomocou metódy Thread.Yield().

Na stav vlákna sa môžeme spýtať pomocou jeho vlastnosti ThreadState. Je to flag nadobúdajúcej jednej alebo viacerých z týchto hodnôt: Running, StopRequested, SuspendRequested, Background, Unstarted, Stopped, WaitSleepJoin, Suspended, AbortRequested, Aborted. Túto vlastnosť používame najmä pri ladení, na synchronizáciu sa nehodí.

Zdieľanie dát medzi vláknami

Často samozrejme potrebujeme medzi vláknami zdieľať nejaké dáta a to minimálne kvôli komunikácii. Určite vás neprekvapí, že ak spustíme tú istú metódu vo viacerých vláknach, v každom vlákne bude mať svoje vlastné lokálne premenné. K jednoduchému pokusu využime triedu z minulého príkladu:

Vypisovac vypisovac = new Vypisovac();
Thread vlakno1 = new Thread(vypisovac.Vypisuj0);
vlakno1.Start();
vypisovac.Vypisuj0();
Console.ReadKey();

výsledok:

Zdieľanie dátami medzi vláknami v C# .NET - Paralelné programovanie a viacvláknové aplikácie v C # .NET

Keďže konzola má v predvolenom stave 80 znakov a sú vypísané necelé 3 riadky, vidíme, že oba cykly prebehli 100x a že každé vlákno použilo svoju premennú i.

ThreadSafety

Metóda vlákna môže pristupovať k inštančným alebo statickým premenným. Práve týmto spôsobom spolu môžu jednotlivé vlákna komunikovať. Ako už tušíme, háčik bude v už spomínanej synchronizáciu.

Predstavme si nasledujúce triedu:

class BankomatUnsafe
{
    private decimal hotovost = 100;

    private void Vyber100()
    {
        if (hotovost >= 100)
        {
            Console.WriteLine("Vybírám 100");
            hotovost -= 100;
            Console.WriteLine("na účtu máte ještě {0}.", hotovost);
        }
    }

    public void VyberVlakny()
    {
        Thread vlakno1 = new Thread(Vyber100);
        vlakno1.Start();
        Vyber100();
        if (hotovost < 0)
            Console.WriteLine("Hotovost je v mínusu, okradli nás.");
    }

}

Trieda reprezentuje bankomat, ktorý eviduje nejakú hotovosť. Tá je pri vytvorení bankomatu 100 Sk. Ďalej disponuje jednoduchou metódou Vyber100(), ktorá vyberie 100 korún v prípade, že je na účte potrebný zostatok. Zaujímavá je pre nás metóda VyberVlakny(), ktorá sa pomocou 2 vlákien (aktuálneho a novovytvoreného) pokúsi vybrať 100 Sk. Ak sa s hotovosťou náhodou dostaneme do mínusu, vypíšeme o tom hlásenie.

Do hlavnej metódy pridáme kód, ktorý vykoná 200 výberov na 100 bankomatoch:

for (int i = 0; i < 100; i++)
{
    BankomatUnsafe bankomat = new BankomatUnsafe();
    bankomat.VyberVlakny();
}

A aplikáciu spustíme:

Synchronizácia vlákien v C# .NET - Paralelné programovanie a viacvláknové aplikácie v C # .NET

Z výpisu vidíme, že niečo nesedí. Kde je problém?

V metóde Vyber100() kontrolujeme podmienkou, či je na účte dostatočná hotovosť. Predstavte si, že je na účte 100 Sk. Podmienka teda platí a systém vlákno uspí treba ihneď za vyhodnotením podmienky. Toto vlákno teda čaká. Druhé vlákno tiež skontroluje podmienku, ktorá platí, a odpočíta 100 Sk. Potom sa prebudí prvé vlákno, ktoré je už za podmienkou a tiež odpočíta 100 Sk. Vo výsledku máme na účte teda záporný zostatok! Vidíme, že práca s vláknami prináša nová úskalia, s ktorými sme sa doteraz ešte nestretli. Situáciu vyriešime pomocou zamykania.

Zamykanie (lock)

Iste sa zhodneme na tom, že sekcia s overením zostatku a jeho následnú zmenou musí prebehnúť vždy celá, inak sa dostávame do vyššie uvedenej situácie. Problém vyriešime tým, že sekciu, kde sa zdieľanou premennou hotovost pracujeme, opatríme zámkom. Kód upravíme do nasledujúcej podoby:

class BankomatSafe
{
    private decimal hotovost = 100;
    private object zamek = new object();

    public void VyberVlakny()
    {
        Thread vlakno1 = new Thread(Vyber100);
        vlakno1.Start();
        Vyber100();
        if (hotovost < 0)
            Console.WriteLine("Hotovost je v mínusu, okradli nás.");
    }

    private void Vyber100()
    {
        lock (zamek)
        {
            if (hotovost >= 100)
            {
                Console.WriteLine("Vybírám 100");
                hotovost -= 100;
                Console.WriteLine("na účtu máte ještě {0}.", hotovost);
            }
        }
    }
}

Zamknutie vykonáme pomocou konštrukcie lock, ktorá berie ako parameter zámok. Zámkom môže byť ľubovoľný objekt, my si za týmto účelom vytvoríme jednoduchý atribút. Keď bude chcieť systém vlákno uspať, musí počkať, až sa dostane z kritickej sekcie (z tej pod zámkom).

Aplikácia teraz funguje ako má a my ju môžeme vyhlásiť za tzv. ThreadSafe (bezpečnú z hľadiska vlákien).

Zamykanie vlákien v C# .NET - Paralelné programovanie a viacvláknové aplikácie v C # .NET

V budúcej lekcii, Monitory, priorita vlákien, výnimky a ďalšie témy v C # .NET , sa zameriame na ďalšie úskalia vlákien, povieme si viac o zamykanie a pustíme sa do predávanie dát do vlákna.


 

Predchádzajúci článok
Úvod do viacvláknových aplikácií v C # .NET
Všetky články v sekcii
Paralelné programovanie a viacvláknové aplikácie v C # .NET
Preskočiť článok
(neodporúčame)
Monitory, priorita vlákien, výnimky a ďalšie témy v C # .NET
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
1 hlasov
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity