2. diel - Vlákna v VB.NET - Sleep, Join a lock
V minulom dieli nášho seriálu tutoriálu o viacvláknových aplikáciách v VB .NET, Úvod do viacvláknových aplikácií v VB.NET , sme si vytvorili prvú viacvláknové aplikácie.
V dnešnom dieli 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 minulého dielu:
Public Class Vypisovac Public Sub Vypisuj0() For index = 1 To 100 Console.Write("0") Thread.Sleep(5) Next End Sub Public Sub Vypisuj1() For index = 1 To 150 Console.Write("1") Thread.Sleep(5) Next End Sub End Class
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":
Sub Main() Dim vypisovac As Vypisovac = New Vypisovac() Dim vlakno1 As Thread = New Thread(AddressOf vypisovac.Vypisuj0) Dim vlakno2 As Thread = New Thread(AddressOf vypisovac.Vypisuj0) vlakno1.Start() vlakno2.Start() Console.WriteLine("Hotovo") Console.ReadKey() End Sub
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:
Sub Main() Dim vypisovac As Vypisovac = New Vypisovac() Dim vlakno1 As Thread = New Thread(AddressOf vypisovac.Vypisuj0) Dim vlakno2 As Thread = New Thread(AddressOf vypisovac.Vypisuj0) vlakno1.Start() vlakno2.Start() vlakno1.Join() vlakno2.Join() Console.WriteLine("Hotovo") Console.ReadKey() End Sub
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:
Dim vypisovac As Vypisovac = New Vypisovac() Dim vlakno1 As Thread = New Thread(AddressOf 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:
Public Class BankomatUnsafe Private hotovost As Decimal = 100 Private Sub vyber100() If hotovost >= 100 Then Console.WriteLine("Vybírám 100") hotovost = hotovost - 100 Console.WriteLine("na účtu máte ještě {0}", hotovost) End If End Sub Public Sub VyberVlakny() Dim vlakno As Thread = New Thread(AddressOf vyber100) vlakno.Start() vyber100() If hotovost < 0D Then Console.WriteLine("Hotovost je v mínusu, okradli nás.") End If End Sub End Class
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 index = 0 To 100 Dim bankomat As BankomatUnsafe = New BankomatUnsafe() bankomat.VyberVlakny() Next
A aplikáciu spustíme:
A 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 zostatok pracujeme, opatríme zámkom. Kód upravíme do nasledujúcej podoby:
Public Class BankomatSafe Private hotovost As Decimal = 100 Private zamek As Object = New Object() Private Sub vyber100() SyncLock zamek If hotovost >= 100 Then Console.WriteLine("Vybírám 100") hotovost -= 100 Console.WriteLine("na účtu máte ještě {0}", hotovost) End If End SyncLock End Sub Public Sub VyberVlakny() Dim vlakno As Thread = New Thread(AddressOf vyber100) vlakno.Start() vyber100() If hotovost < 0D Then Console.WriteLine("Hotovost je v mínusu, okradli nás.") End If End Sub End Class
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, Vlákna - Bezpečnosť vlákien vo VB.NET , sa budeme zaoberať bezpečnosťou vlákien vo VB.NET. Vyberieme si jednu techniku zdieľania dát medzi vláknami, ktorú implementujeme do príkladu.