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é:
"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:
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:
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:
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).
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.