Integrované prostředí rubriky Visual Basic

Autor: Ján Hanák

Téma měsíce

Programování vícevláknových aplikací

Časová náročnost

(min):

65

Začátečník

Pokročilý

Profesionál

Použitý operační systém : Hlavní vývojový nástroj :

Další vývojový software :

Jiný software :

Windows 2000 SP3

Visual Basic .NET 2002

Žádný

Žádný

 

Nepřehlédněte exkluzivní Speciál pro programátory, který pojednává o jazykové interoperabilitě mezi jazyky Managed Extensions for C++ a Visual Basic .NET.

 

Milí čtenáři,

 

32bitové prostředí operačního systému Windows s sebou přineslo několik podstatných a zásadních změn, a to nejen z uživatelského, nýbrž i z programovacího pohledu. Na přední příčku v řebříčku „top“ změn se řadí také plná podpora multitaskingu (paralelního zpracování většího množství úloh) a multithreadingu (podpora exekuce vícevláknových aplikací). Jak naprogramovat vícevláknovou aplikaci se dozvíte právě v tomto vydání sekce Téma měsíce.

 

Obsah

Teoretický úvod do problematiky procesů a vláken

Vytváříme první vícevláknovou aplikaci

Testujeme první vícevláknovou aplikaci

Operace s vlákny

 

 

Teoretický úvod do problematiky procesů a vláken

 

Abychom mohli mluvit o programování vícevláknových aplikací na patřičné úrovni, musíme si nejprve povědět pár slov k „vícevláknové“ terminologii. Začneme od definice pojmu aplikace, který je vám pravděpodobně nejbližší. Aplikace je definována jako soubor programového kódu, dat a dalších interních nebo externích zdrojů, které společně vytvářejí jeden celek, jenž řeší jistou smysluplnou úlohu. Pojem aplikace se někdy volně zaměňuje s termínem počítačový program.

 

I když je ve většině případů tato substituce možná, není zcela přesná. A to z toho důvodu, že programem může být také jenom několik řádků programového kódu, které ač řeší jistý úkol, nemusí být nutně samostatným celkem, jenž je předložen před uživatele. Můžeme však říct, že aplikace je již hotový, odladění počítačový program, jenž se stává předmětem prodeje na softwarovém trhu.

 

V .NET prostředí je kód aplikace uložen v jedné, nebo i několika assembly. Assembly je pak uložena v přenositelném spustitelném souboru PE (Portable Executable). Jestliže je soubor aktivován (poklepáním na jeho ikonu), potřebný programový kód v podobě jazyka MSIL bude přeložen JIT kompilátorem, umístěn do paměti počítače a podroben přímé exekuci. V tomto okamžiku říkáme, že aplikace běží. V programování se běžící aplikace označuje jako proces. Pro potřeby procesu je alokován jistý paměťový prostor, ve kterém se daný proces nachází. 32bitový operační systém Windows je, jak již bylo řečeno, plně vybaven pro správu více procesů, resp. úkolů. Jak je to možné? Velmi zjednodušeně můžeme prohlásit, že každému procesu operační systém přičlení stanovený časový interval, v rámci kterého je procesor (CPU) počítače plně zaměstnán prováděním kódu daného procesu. Po uplynutí vymezeného intervalu se operační systém „přepne“ do jiného procesu a jistou dobu vykonává kód tohoto procesu. Tímto způsobem operační systém spravuje všechny procesy, neboli spuštěné aplikace. Je důležité si uvědomit, že práce s procesy je kompletně v rukou operačního systému, nikoliv jiných procesů.

 

Každý proces je tvořen přinejmenším jedním vláknem (thread). Vlákno lze definovat jako samostatnou jednotku exekuce programového kódu procesu. Každé vlákno má své identifikační číslo (ID), jméno a prioritu. Zatímco jméno vlákna tvoří textový řetězec, jenž slouží na uživatelskou identifikaci vlákna, identifikační číslo vlákno přesně charakterizuje z pohledu operačního systému. Priorita vlákna určuje, jak intenzivně je zapotřebí programový kód daného vlákna provádět. Zvyčejně mají vlákna normální prioritu, avšak v případě potřeby je možné zvýšit, nebo naopak snížit prioritu vlákna na příslušnou úroveň. Jestliže proces obsahuje jenom jedno vlákno, mluvíme o jednovláknové aplikaci (single threaded application). Pokud jste vyvíjeli aplikace v předchozí verzi Visual Basicu, nejspíš jste pracovali pouze s jednovláknovými aplikacemi. Visual Basic .NET ovšem nabízí programování také vícevláknových aplikací (multithreaded applications). Vícevláknová aplikace je aplikace, která používá dvě a více vláken. Komparaci jedno a vícevláknové aplikace můžete vidět na obr. 1.

 

 

Obr. 1 – Jedno a vícevláknová aplikace

 

Vlákno, které je na obrázku pojmenováno jako „Vlákno 1“ je základním vláknem procesu, což znamená, že toto vlákno je vytvořeno implicitně běhovým prostředím aplikace. Naproti tomu vlákno s názvem „Vlákno 2“ je pracovním vláknem, které vytvořil programátor pro své potřeby. Ačkoliv obrázek představuje jenom dvouvláknovou aplikaci, proces může obsahovat i větší množství vláken. Příliš mnoho vláken ovšem není žádoucí, protože by snadno mohly nastat potíže se synchronizací jednotlivých vláken (pomineme-li poněkud větší paměťovou náročnost aplikace).  

 

Každému vláknu v procesu je přiřazen jistý časový interval, během kterého CPU počítače realizuje programové instrukce daného vlákna. Jakmile tento interval uplyne, CPU okamžitě začne zpracovávat kód dalšího vlákna. Jelikož je časový interval realizace kódu vlákna velmi malý, vizuálně to vypadá, jakoby obě vlákna pracovala současně. 

 

 

Vytváříme první vícevláknovou aplikaci

 

V následující programové ukázce si předvedeme, jak sestrojit jednoduchou dvouvláknovou aplikaci. Postupujte podle níže uvedených instrukcí:

 

  1. Spusťte Visual Basic .NET a vytvořte standardní aplikace pro Windows (Windows Application).

 

  1. Na formulář přidejte jednu instanci ovládacího prvku Button, kterou pojmenujte jako btnVytvořit_vlákno (libovolně můžete také modifikovat vlastnost Text vytvořené instance). Na vytvořenou instanci poklepejte, čímž nařídíte Visual Basicu, aby vygeneroval kostru událostní procedury Click instance. Do obslužné procedury zadejte tento programový kód:

 

        Dim NovéVlákno As New Threading.Thread(AddressOf Metoda)

        NovéVlákno.Name = "MojeVlákno"

        NovéVlákno.Start()

 

Pokaždé, když budeme chtít vytvořit nové vlákno, použijeme třídu Thread z jmenného prostoru System.Threading. Z tohoto pohledu je tedy zřejmé, že k vytvořenému vláknu budeme ve skutečnosti přistupovat prostřednictvím instance třídy Thread. Instance této třídy se ovšem rodí poněkud zvláštním způsobem. Podívejme se na něj blíže. Především je zapotřebí obsloužit parametrický konstruktor třídy Thread, kterému je nutno předat (pomocí operátoru AddressOf) runtime adresu procedury, která bude prováděna na nově vytvořeném vlákně. Ve skutečnosti je však celý proces poněkud komplikovanější: Ačkoliv je tato skutečnost zastíněna, je vytvořen delegát ThreadStart, jemuž je svěřena runtime adresa spouštěcí procedury. I když vytvoříme nové vlákno (resp. instanci třídy Thread), neznamená to ještě, že jsme také toto vlákno spustili.

 

V této souvislosti je nutno podotknout, že při práci s vlákny je zapotřebí důsledně rozlišovat dva pojmy, a sice vytvoření vlákna a jeho spuštění. Vlákno je vytvořeno jakmile je zrozena instance třídy Thread. Takto vytvořené vlákno má příznak Unstarted, což znamená, že ještě nebylo spuštěno. Aby mohlo být vlákno spuštěno, musí být zavolána metoda Start instance třídy Thread.   

 

Spuštění nového vlákna zabezpečíme zavoláním metody Start instance třídy Thread. Jakmile je aktivována metoda Start, je zavolána spouštěcí procedura, která bude prováděna na novém vlákně. Proces tvorby a spuštění nového vlákna je znázorněn na obr. 2.

 

 

Obr. 2 – Proces vytvoření a spuštění vlákna

 

Pro zvídavé programátory: Vytvoření instance třídy Thread pod drobnohledem

 

Ještě jednou se podívejme na výše uvedený zdrojový kód pro vytvoření instance třídy Thread:

 

        Dim NovéVlákno As New Threading.Thread(AddressOf Metoda)

        NovéVlákno.Name = "MojeVlákno"

        NovéVlákno.Start()

 

I když je tento kód zcela správný, není v něm vidět role delegáta ThreadStart. Obměníme-li kód následovně, vyjdou skryté skutečnosti na povrch:

 

        Dim NovéVlákno As New Threading.Thread _

        (New Threading.ThreadStart(AddressOf Metoda))

        NovéVlákno.Name = "MojeVlákno"

        NovéVlákno.Start()

 

Tento kód jasně demonstruje způsob, pomocí něhož je instanci třídy Thread předán delegát ThreadState, kterému je prostřednictvím operátoru AddressOf poskytnuta runtime adresa spouštěcí procedury (s názvem Metoda), která bude realizována na nově vytvořeném vlákně. Ačkoliv je tento zápis kódu pro vytvoření vlákna znatelně delší, umožňuje nám pohled do zákulisí procesu vytváření vlákna. V mnoha programech jej však zcela jistě neuvidíte, protože Visual Basic přidává uvedené rozšíření kódu implicitně, a tedy není nutné, abyste tuto „prodlouženou“ verzi kódu používali ve svých aplikacích.

 

 

V našem případě je vytvořena instance třídy Thread s názvem NovéVlákno. Konstruktoru třídy je předána adresa procedury Metoda, tato procedura se tedy bude provádět na druhém (vytvořeném) vlákně. Ještě předtím, než je vlákno spuštěno, je upravena vlastnost Name instance MojeVlákno – nové vlákno tak bude disponovat uživatelsky přívětivým jménem.

 

  1. Jestliže jste událostní proceduru Click instance tlačítka vyplnili uvedeným zdrojovým kódem, obdrželi jste zcela jistě připomínku, že procedura Metoda není deklarována. Proto je nutné kód této procedury zahrnout do kódu třídy formuláře Form1. Protože ukázková procedura Metoda využívá referenční proměnné a konstanty s oborem třídy, je uveden výpis kompletního programového kódu, jenž se nachází v souboru Form1.vb:

 

Option Strict On

Public Class Form1

    Inherits System.Windows.Forms.Form

 

    Dim frm As Form

    Dim lblText1 As Label

    Dim lbltext2 As Label

    Dim Progres As ProgressBar

 

    Const PROGRES_MIN As Integer = 1

    Const PROGRES_MAX As Integer = 1000

 

    Private Sub Button1_Click(ByVal sender As System.Object, _

    ByVal e As System.EventArgs) Handles Button1.Click

        Dim NovéVlákno As New Threading.Thread(AddressOf Metoda)

        NovéVlákno.Name = "MojeVlákno"

        NovéVlákno.Start()

    End Sub

 

    Private Sub Metoda()

        frm = New Form()

        lblText1 = New Label()

        lbltext2 = New Label()

        Progres = New ProgressBar()

        With frm

            .ControlBox = False

            .FormBorderStyle = FormBorderStyle.FixedDialog

            .Text = "Probíhá výpočet náhodných čísel..."

        End With

 

        With lblText1

            .Location = New Point(0, 0)

            .Size = New Size(frm.Width, frm.Height \ 4)

            .Text = "Právě probíhá generování náhodných čísel"

            .Font = New Font("Verdana", 18, FontStyle.Bold, _

            GraphicsUnit.Pixel)

            .TextAlign = ContentAlignment.MiddleCenter

        End With

 

        With lbltext2

            .Location = New Point(0, 75)

            .Size = New Size(frm.Width, frm.Height \ 6)

            lbltext2.Text = "Číselný průbeh generování čísel:"

            .Font = New Font("Verdana", 11, FontStyle.Bold, _

            GraphicsUnit.Pixel)

            .TextAlign = ContentAlignment.MiddleCenter

        End With

 

        With Progres

            .Dock = DockStyle.Bottom

            .Height = frm.Height \ 6

            .Minimum = PROGRES_MIN

            .Maximum = PROGRES_MAX

        End With

 

        With frm.Controls

            .Add(Progres)

            .Add(lblText1)

            .Add(lbltext2)

        End With

 

 

        AddHandler frm.Load, AddressOf frm_Load

        frm.Show()

 

        Dim f As Integer = FreeFile()

        Randomize()

        Dim a, x As Integer

        FileOpen(f, "d:\data.txt", OpenMode.Output)

        For a = PROGRES_MIN To PROGRES_MAX

            x = CInt(Int(1000 * Rnd()) + 1))

            Progres.Value = a

            lbltext2.Text = a & " / " & PROGRES_MAX

            If a < PROGRES_MAX Then

                Print(f, x & ", ")

            Else

                Print(f, x & ".")

            End If

            Application.DoEvents()

        Next a

        FileClose(f)

        frm.Close()

 

    End Sub

 

    Private Sub frm_Load(ByVal sender As Object, ByVal e As EventArgs)

        frm.Top = (Screen.PrimaryScreen.Bounds.Height - frm.Height) \ 2

        frm.Left = (Screen.PrimaryScreen.Bounds.Width - frm.Width) \ 2

    End Sub

End Class

 

Na vytvořeném vlákně bude prováděn kód procedury Metoda, který realizuje následující činnosti:

 

 

 

Testujeme první vícevláknovou aplikaci

 

Jste-li s programováním hotovi, můžete aplikaci sestavit (Build è Build Solution). Jestliže aplikaci spustíte, uvidíte, že svoji práce odvádí skvěle (obr. 2).

 

 

Obr. 2 – Vícevláknová aplikace v akci

 

Jak se ovšem dovíme, že generování náhodných čísel, jako i další kód, jenž je uložen v proceduře Metoda skutečně běží na nově vytvořeném vlákně? Inu, asi takto:

 

  1. Pokud se váš projekt ještě nachází v režimu běhu, tak uzavřete dialogové okno aplikace, čímž se dostanete do režimu návrhu.
  2. V programovém kódu třídy Form1 vyhledejte začátek procedury Metoda.
  3. Abychom viděli, zdali je již vytvořené nové vlákno, umístíme na začátek procedury Metoda programovou zarážku (breakpoint). To uděláte tak, že nalevo od názvu procedury klepnete na plochu šedého panelu (obr. 3).

 

 

Obr. 3 – Vložení zarážky do programového kódu

 

O správnosti umístění zarážky vás ujistí červený puntík, jenž se objeví v levém panelu. Kromě červeného puntíku si můžete také všimnout červený pruh, kterým je zvýrazněn řádek programového kódu, na němž je zarážka umístěná.

 

Jestliže na nějaký řádek programového kódu umístíme zarážku, dáme tím debuggeru Visual Basicu příkaz, aby při přechodu na řádek se zarážkou zastavil běh aplikace a zpřístupnil dodatečné možnosti pro odlaďování kódu aplikace. 

 

Zařazení programové zarážky je výhodné především proto, že jakmile debugger Visual Basicu „skočí“ na řádek se zarážkou, bude běh aplikace pozastaven a my budeme moci prozkoumat vnitřní programové jádro aplikace. Pozastavenou aplikaci lze samozřejmě dále rozběhnout, nebo ji můžete ukončit.

 

  1. Spusťte aplikaci a klepněte na tlačítko pro vytvoření nového vlákna. Běh aplikace se zastaví a vy budete přeneseni do režimu odlaďování aplikace.
  2. Seznam vláken je uveden v dialogu Threads. Pokud tento dialog nevidíte, vyberte nabídku Debug, ukažte na subnabídku Windows a klepněte na položku Threads. Zobrazí se dialog Threads, v němž jsou uvedena všechna aktivovaná vlákna (obr. 4).  

 

Dialogové okno Threads můžete zobrazit také pomocí klávesové zkratky CTRL+ALT+H.

 

 

Obr. 4 – Dialogové okno Threads

 

  1. Všimněte si, že v okamžiku přerušení běhu aplikace již bylo vytvořeno nové vlákno. Toto vlákno má název MojeVlákno, identifikační číslo 1324 a normální prioritu.
  2. Pokud chcete, aby aplikace pokračovala v běhu, vyberte nabídku Debug a klepněte na položku Continue. V opačném případě můžete zvolit příkaz Stop Debugging (rovněž z nabídky Debug).
  3. Vytvořenou programovou zarážku odstraníte klepnutím na červení puntík v levém panelu.

 

 

Operace s vlákny

 

Jakmile je vlákno vytvořeno, můžete s ním provádět několik operací. Rámec potenciálních akcí pro práci s vlákny zastřešuje třída Thread se svými členskými metodami a vlastnostmi. V následující tabulce se nachází přehled některých klíčových metod a vlastností pro práci s vlákny, o nichž si myslím, že by mohly být pro vás užitečné.

 

Název

Typ členu

Charakteristika

Abort

Metoda

Metoda se používá pro zrušení vlákna. Po zavolání metody je ve vybraném vláknu generována výjimka ThreadAbortException, nacož je spuštěn proces rušení vlákna.

CurrentThread

Vlastnost

Jde o sdílenou (shared) vlastnost, což znamená, že jde o vlastnost samotné třídy Thread a nikoliv instance této třídy. Použijete-li tuto vlastnost, získáte přístup k vláknu, jehož programový kód je právě prováděn. 

Name

Vlastnost

Pokud chcete vlákno pojmenovat, případně chcete-li jméno vlákna získat, můžete použít tuto vlastnost. Hodnotou vlastnosti je textový řetězec, jenž představuje uživatelské jméno pro požadované vlákno.

Priority

Vlastnost

Vlastnost určuje prioritu vlákna. Každé vlákno se může nacházet v několika prioritních stavech:

·          Highest (Nejvyšší priorita)

·          AboveNormal (Vyšší nežli normální priorita)

·          Normal (Standardní priorita)

·          BelowNormal (Nižší nežli normální priorita)

·          Lowest (Nejnižší priorita)

Hodnotou vlastnosti je jeden z členů enumerace ThreadPriority (AboveNormal, BelowNormal, Highest, Lowest, Normal).

ResetAbort

Metoda

Jde o sdílenou (shared) metodu, která likviduje požadavek na zrušení (Abort) vlákna.

Resume

Metoda

Metodu lze aplikovat na vlákno, které bylo uvedeno do stavu nečinnosti použitím metody Suspend. Metoda zabezpečí probuzení vlákna a exekuci jeho programového kódu. 

Sleep

Metoda

Jde o sdílenou (shared) a také přetíženou metodu, která se vyskytuje ve dvou exemplářích. Obě přetížené varianty metody zamezí provádění programového kódu aktivního vlákna na určitý časový interval (tento je zvyčejně měřen v milisekundách).    

Start

Metoda

Zavolání metody způsobí rozběhnutí programového kódu na daném vlákně. Vláknu, resp. instanci třídy Thread, je po aktivaci metody Start přiřazen stav ThreadState.Running, což znamená, že vlákno je aktivní (je realizován kód vlákna). Jestliže bylo vlákno jednou ukončeno, nelze jej opět aktivovat prostřednictvím opětovného volání metody Start.  

Suspend

Metoda

Metoda Suspend je podobně jako metoda Sleep používaná na „uvedení vlákna do stavu spánku“. Mezi těmito dvěma metodami ovšem existuje několik podstatných rozdílů. Především, metoda Sleep je sdílená (shared), což znamená, že ji možno aplikovat jenom na vlákno, jehož programový kód se právě uskutečňuje. Metoda Suspend je naproti tomu metodou instance a nikoliv třídy, z čehož vyplývá, že tuto metodu můžete použít na jakoukoliv instanci třídy Thread. Druhým odlišným znakem je skutečnost, že jestliže je použita metoda Sleep, je předem definovaný časový interval (v milisekundách), v rámci něhož bude vlákno spát. Na druhé straně, bude-li zavolána metoda Suspend, nelze předem určit, kdy má dojít k uspání vlákna. Ve skutečnosti dochází k uspání vlákna v okamžiku, kdy je dosažen tzv. bezpečný bod. Určení existence bezpečného bodu má na starosti Common Language Runtime (CLR) pomocí služby Garbage Collection. A konečně, poslední odlišnost spočívá v opětovné aktivaci deaktivovaného vlákna. Bude-li vlákno uspáno pomocí metody Suspend, lze jej ihned probudit zavoláním metody Resume. Uspíte-li však vlákno metodou Sleep, neexistuje žádný způsob, jak jej probudit předtím, nežli vyprší stanovená doba vlákna pro nečinnost.

ThreadState

Vlastnost

Každé vlákno se může nacházet v určitém počtu stavů. Skutečnosti o stavu vlákna zprostředkovává právě vlastnost ThreadState. Jde o vlastnost pouze pro čtení, což znamená, že hodnotu vlastnosti lze přečíst, ovšem není možné ji jakkoliv modifikovat (toto chování je vzhledem k charakteru vláken snadno pochopitelné). Hodnotou vlastnosti je jeden z členů enumerace ThreadState.   

 

Tab. 1 – Charakteristika užitečných vlastností a metod třídy Thread

 

 

Právě jste dočetli Téma měsíce. 

 

Jestliže se chcete dozvědět více informací o programování ve Visual Basicu .NET, neváhejte a navštivte také další sekce rubriky Visual Basic. A jakáže je dnešní nabídka?

Seriál Začínáme s VB .NET

Klasifikace operátorů, aritmetické a porovnávací operátory.

Programátorská laboratoř

Tvorba zapečetěné třídy

Přístup k registrům operačního systému Windows

Vytváření vlastního delegáta

 

Nepřehlédněte exkluzivní Speciál pro programátory, který pojednává o jazykové interoperabilitě mezi jazyky Managed Extensions for C++ a Visual Basic .NET.