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

Ostatní jazyky Visual Basic .NET Paralelní programování 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 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í:

Vícevláknová aplikace ve VB.NET bez Join

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

Vícevláknová aplikace ve VB.NET s Join

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:

Vícevláknové aplikace ve VB.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:

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:

Threadsafety v VB.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:

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

Threadsafe aplikace ve VB.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 Ondřej Štorc
Avatar
Autor se věnuje C#, HTML a CSS. Snaží se proniknout do tajů PHP, JS a ASP.NET

Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!


 



 

 

Komentáře

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.

Zatím nikdo nevložil komentář - buď první!