Lekce 2 - Vlákna v C# .NET - Sleep, Join a lock
V minulé lekci, Úvod do vícevláknových aplikací v C# .NET, jsme si vytvořili první vícevláknovou aplikaci.
V dnešním C# .NET tutoriá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é lekce:
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í:

"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í:

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:

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:

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 hotovost
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).

V příští lekci, Monitory, priorita vláken, výjimky a další témata v C# .NET, 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.
Komentáře


Zobrazeno 10 zpráv z 13. Zobrazit vše