Lekce 2 - Vlákna v VB.NET - Sleep, Join a lock
V minulém dílu našeho seriálu tutoriálu o vícevláknových aplikacích v VB .NET, Úvod do vícevláknových aplikací v VB.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:
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
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":
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 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:
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 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:
Dim vypisovac As Vypisovac = New Vypisovac() Dim vlakno1 As Thread = New Thread(AddressOf 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:
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
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 index = 0 To 100 Dim bankomat As BankomatUnsafe = New BankomatUnsafe() bankomat.VyberVlakny() Next
A aplikaci spustíme:

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

Příště, Monitory, priorita vláken, výjimky a další témata v VB.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.