2. díl - Vlákna v C# .NET - Sleep, Join a lock

C# .NET Paralelní programování Vlákna v C# .NET - Sleep, Join a lock

V minulém dílu našeho seriálu tutoriálu o vícevláknových aplikacích v C# .NET jsme si vytvořili první vícevláknovou aplikaci. V dnešním dílu se naučíme vlákna blokovat a zamykat.

Sleep a Join

Aktuální vlákno můžeme uspat na daný počet milisekund a to pomocí statické metody Sleep na třídě Thread. Vlákno je blokováno dokud čas nevyprší, poté se opět probouzí a pokračuje ve své činnosti.

Vytvořme si nový projekt s třídou Vypisovac, která bude vypadat podobně, jako Prepinac z minulého dílu:

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);
                }
        }

}

Metoda Vypisuj0() vypíše do konzole 100 nul a při každém výpisu uspí své vlákno na 5ms. Vypisuj1() vypíše 150 jedniček a poběží tedy déle (asi o 1/4 vteřiny) než metoda Vypisuj0().

Nyní v hlavní metodě vytvoříme vlákno pro každou metodu a vlákna spustíme. Nakonec 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 aplikace je následující:

Join vláken v C# .NET

"Hotovo" se vypsalo jako první, protože hlavní vlákno nečekalo na vypisovací vlákna. Na dokončení činnosti vlákna můžeme počkat a to pomocí metody Join(), která zablokuje aktuální vlákno, dokud se metoda nedokončí. Upravme náš kód do následující 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 nyní čeká až obě vlákna dokončí svou práci. Výsledek je následující:

Join vláken v C# .NET

Pokud bychom chtěli nějaké vlákno uspat na dlouhou dobu, můžeme místo přepočítávání hodin na sekundy předat v parametru instanci TimeSpan. Třída TimeSpan má statické metody jako FromHours() a podobně:

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

Pokud chceme, aby systém nějaké vlákno přepnul, můžeme ho nechat spát i na 0 ms. Samotné volání Thread.Sleep() vlákno vždy zablokuje. Podobného efektu můžeme dosáhnout pomocí metody Thread.Yield().

Na stav vlákna se můžeme zeptat pomocí jeho vlastnosti ThreadState. Je to flag nabývající jedné nebo několika z těchto hodnot: Running, StopRequested, SuspendRequested, Background, Unstarted, Stopped, WaitSleepJoin, Suspended, AbortRequested, Aborted. Tuto vlastnost používáme zejména při ladění, k synchronizaci se nehodí.

Sdílení dat mezi vlákny

Často samozřejmě potřebujeme mezi vlákny sdílet nějaká data a to minimálně kvůli komunikaci. Určitě vás nepřekvapí, že pokud spustíme tu samou metodu ve více vláknech, v každém vláknu bude mít své vlastní lokální proměnné. K jednoduchému pokusu využijme třídu z minulého příkladu:

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

Výsledek:

Sdílení daty mezi vlákny v C# .NET

Jelikož konzole má ve výchozím stavu 80 znaků a jsou vypsané necelé 3 řádky, vidíme, že oba cykly proběhly 100x a že každé vlákno použilo svou proměnnou i.

ThreadSafety

Metoda vlákna může přistupovat k instančním nebo statickým proměnným. Právě tímto způsobem spolu mohou jednotlivá vlákna komunikovat. Jak již tušíme, háček bude v již zmíněné synchronizaci.

Představme si následující třídu:

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.");
        }

}

Třída reprezentuje bankomat, který eviduje nějakou hotovost. Ta je při vytvoření bankomatu 100 Kč. Dále disponuje jednoduchou metodou Vyber100(), která vybere 100 korun v případě, že je na účtu potřebný zůstatek. Zajímavá je pro nás metoda VyberVlakny(), která se pomocí 2 vláken (aktuálního a nově vytvořeného) pokusí vybrat 100 Kč. Pokud se s hotovostí náhodou dostaneme do mínusu, vypíšeme o tom hlášení.

Do hlavní metody přidáme kód, který provede 200 výběrů na 100 bankomatech:

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

A aplikaci spustíme:

Synchronizace vláken v C# .NET

A z výpisu vidíme, že něco nesedí. Kde je problém?

V metodě Vyber100() kontrolujeme podmínkou, zda je na účtu dostatečná hotovost. Představte si, že je na účtu 100 Kč. Podmínka tedy platí a systém vlákno uspí třeba ihned za vyhodnocením podmínky. Toto vlákno tedy čeká. Druhé vlákno také zkontroluje podmínku, která platí, a odečte 100 Kč. Poté se probudí první vlákno, které je již za podmínkou a také odečte 100 Kč. Ve výsledku máme na účtu tedy záporný zůstatek! Vidíme, že práce s vlákny přináší nová úskalí, se kterými jsme se doposud ještě nesetkali. Situaci vyřešíme pomocí zamykání.

Zamykání (lock)

Jistě se shodneme na tom, že sekce s ověřením zůstatku a jeho následnou změnou musí proběhnout vždy celá, jinak se dostáváme do výše zmíněné situace. Problém vyřešíme tím, že sekci, kde se sdílenou proměnnou zustatek pracujeme, opatříme zámkem. Kód upravíme do následující 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);
                        }
                }
        }
}

Zamknutí provedeme pomocí konstrukce lock, která bere jako parametr zámek. Zámkem může být libovolný objekt, my si za tímto účelem vytvoříme jednoduchý atribut. Když bude chtít systém vlákno uspat, musí počkat, až se dostane z kritické sekce (z té pod zámkem).

Aplikace nyní funguje jak má a my ji můžeme prohlásit za tzv. ThreadSafe (bezpečnou z hlediska vláken).

Zamykání vláken v C# .NET

Příště se zaměříme na další úskalí vláken, řekneme si více o zamykání a pustíme se do předávání dat do vlákna.


 

  Aktivity (1)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Celkem (12 hlasů) :
55555


 



 

 

Komentáře

Avatar
Člen
Člen
Avatar
Člen:

Pekny clanok :)

Odpovědět 5.9.2014 10:19
...
Avatar
Ondřej Krsička
Redaktor
Avatar
Ondřej Krsička:

Na co je tam ten objekt zamek? Zkusil jsem do lock dát new object() a to nefungovalo.

 
Odpovědět 21. dubna 21:38
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 2 zpráv z 2.