BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Implementowanie procesu działającego w tle w Visual Basic .NET

11-10-2003 21:18 | User 107543

Wielowątkowość pozwala aplikacji na wykonywanie więcej niż jednego zadania na raz. Wykorzystując wielowątkowość można uruchomić jeden wątek obsługujący interfejs użytkownika i drugi wątek – wykonujący w tle intensywne obliczenia lub przetwarzanie. Microsoft® Visual Basic® .NET obsługuje wielowątkowość, więc łatwo można z tego potencjału skorzystać.

Niestety, wielowątkowość ma także ciemniejszą stronę. Zawsze gdy aplikacja wykorzystuje więcej niż jeden wątek, problemy mogą się pojawić w momencie, gdy kilka wątków spróbuje użyć tych samych danych lub zasobów jednocześnie. Wtedy sprawy zwykle się komplikują i znalezienie błędów staje się trudne.

Co gorsza, podczas tworzenia często wydaje się, że  wielowątkowy kod pracuje poprawnie, natomiast podczas normalnego używania powoduje błąd z powodu nie wykrytego przypadku jednoczesnego wykorzystania tych samych danych lub zasobów przez wiele wątków. Właśnie przez to programowanie wielowątkowe jest bardzo ryzykowne!  

Z powodu trudności związanych z projektowaniem oraz usuwaniem błędów („odpluskwianiem”) wielowątkowych aplikacji Microsoft stworzył w COM koncepcję „jednowątkowego pokoju” (ang. single-threaded apartment – STA). Kod Visual Basic 6 zawsze działał w STA, więc utworzony kod musiał się troszczyć tylko o pojedynczy wątek. W ten sposób uniknięto problemów związanych ze współdzielonymi danymi lub zasobami, ale jednocześnie oznaczało to brak możliwości wykorzystania wielowątkowości bez odwoływania się do drastycznych środków.

W .NET nie ma czegoś takiego, jak STA. Kod .NET działa zawsze w ramach AppDomain (domeny aplikacji), która umożliwia wielowątkowość. To oznacza, że kod Visual Basic .NET także działa w AppDomain i tym samym może wykorzystywać wielowątkowość. Oczywiście przy każdym wykorzystaniu tej możliwości kod należy pisać bardzo ostrożnie, by uniknąć konfliktów pomiędzy wątkami.

Najprostszym jednak sposobem uniknięcia tego typu konfliktów jest upewnienie się, że poszczególne wątki nigdy nie będą korzystać z tych samych danych oraz zasobów. Choć nie zawsze jest to możliwe, zminimalizowanie wykorzystania współdzielonych danych oraz zasobów powinno być celem każdego projektu wielowątkowej aplikacji – nie tylko upraszcza to kodowanie oraz odpluskwianie, ale także zwiększa wydajność rozwiązania (dlatego że nie ma potrzeby by wątki „czekały” na zwolnienie danego zasobu).

Aby możliwe było rozwiązywania konfliktów pomiędzy wątkami, konieczne jest zastosowanie technik synchronizacyjnych, które często powodują blokowanie – tzn. tymczasowe zatrzymywanie – wątków, aż do momentu zakończenia przez inny wątek czynności wywołującej konflikt. Zablokowanie wątku oznacza, że jest on bezczynny, a nie wykonując żadnej pracy zmniejsza wydajność.

Przyciski anulowania oraz wyświetlacze statusu

Istnieje wiele przyczyn użycia wielowątkowości w aplikacji, ale jedną z najpowszechniejszych jest konieczność zapewnienia takiego mechanizmu, by całość lub części interfejsu użytkownika była w stanie „reagować” na działanie tego użytkownika nawet podczas wykonywania długotrwałego zadania.

Jako absolutne minimum zwykle wymagane jest utrzymanie aktywności przycisku Anuluj (Cancel),  tak aby użytkownicy mogli zasygnalizować chęć przerwania długotrwałego zadania.

W Visual Basic 6 próbuje się to robić poprzez wymuszanie obsługi zdarzeń za pomocą DoEvents oraz kontrolek-stoperów i wiele innych „sztuczek”. W Visual Basic .NET jest to znacznie prostsze, ponieważ możliwe jest użycie wielowątkowości i – przy zachowaniu ostrożności można to zrobić bez radykalnego zwiększania stopnia komplikacji kodowania oraz odpluskwiania.

Kluczem do sukcesu podczas implementowania w wielowątkowym środowisku przycisku Anuluj (Cancel) jest pamiętanie, że ten przycisk tylko wysyła żądanie anulowania zadania. To do zadania działającego w tle należy zatrzymanie obliczeń we właściwym momencie.

Gdyby zaimplementowano przycisk Anuluj (Cancel) bezpośrednio zatrzymujący proces działający w tle, mogłoby się to zdarzyć w środku jakiejś newralgicznej operacji lub zanim ten proces zamknąłby kosztowne zasoby, takie jak uchwyty do plików lub połączenia z bazami danych. Mogłoby to być katastrofalne w skutkach – prowadziłoby do utknięcia w martwym punkcie, niestabilnego zachowania aplikacji lub zwyczajnej awarii aplikacji prowadzącej do jej wyrzucenia z systemu.

Dlatego przycisk Anuluj (Cancel) powinien zawsze wysyłać do wątku działającego w tle żądanie zatrzymania. Zadanie z tła może wtedy sprawdzić, czy żądanie anulowania zostało wydane we właściwym momencie i  wówczas wątek działający w tle może zwolnić wszelkie zasoby, zatrzymać każde newralgiczne działanie i bez problemu zakończyć swoje działanie.

Samo żądanie anulowania operacji jest ważne, zwykle jednak pożądane będzie także wyświetlanie przez interfejs użytkownika informacji z procesu działającego w tle. Mogą to być komunikaty tekstowe, stopień wykonania zadania lub jedno i drugie.

Główną komplikacją, z jaką trzeba się zmierzyć podczas implementowania przycisku Anuluj (Cancel) lub wyświetlacza statusu Visual Basic .NET, jest to, że biblioteka WinForms (formatki Windows) nie jest bezpieczna pod względem wątków. Oznacza to po prostu, że tylko taki wątek, który utworzył formatkę, może wchodzić w interakcje z tym formatkami oraz jego kontrolkami. Żaden inny wątek nie może bezpiecznie zmieniać np. treści wyświetlanej w etykiecie.

Niestety, nic nie jest w stanie powstrzymać programisty od napisania kodu, w którym wiele wątków wchodzi w interakcje z daną formatką. To znaczy nic, poza nieprzewidywalnymi skutkami oraz potencjalnymi awariami aplikacji, które mogą wystąpić po uruchomieniu...

To oznacza, że kod trzeba pisać bardzo ostrożnie – tak, aby być pewnym, że tylko wątek interfejsu użytkownika wchodzi w interakcje z samym interfejsem użytkownika. Aby to zapewnić, można stworzyć prostą strukturę zarządzającą interakcjami pomiędzy procesem roboczym działającym w tle a wątkiem interfejsu użytkownika. Jeśli wszystko zostanie zrobione poprawnie, możliwe jest utrzymanie względnej przezroczystości wykorzystania wielowątkowości zarówno dla kodu interfejsu użytkownika, jak i kodu tego długotrwałego zadania.

Wątki oraz obiekty

Najprostszym sposobem na stworzenie procesu działającego w tle uruchamianego w jego własnym wątku i z jego własnymi danymi jest stworzenie obiektu przeznaczonego specjalnie dla procesu działającego w tle. Choć cel ten nie zawsze jest osiągalny, jest to dobry cel, ponieważ znacznie upraszcza tworzenie wielowątkowych aplikacji.

Gdy wątek działający w tle działa w ramach swojego własnego obiektu, może wykorzystywać zmienne instancji obiektu (zmienne deklarowane wewnątrz klasy) bez konieczności „martwienia się” o to, że zostaną one wykorzystane przez inne wątki. Rozważmy dla przykładu poniższą klasę:

Public Class Worker

 Private mInner As Integer

 Private mOuter As Integer

 

 Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer)

   mInner = InnerSize

   mOuter = OuterSize

 End Sub

 

 Public Sub Work()

   Dim innerIndex As Integer

   Dim outerIndex As Integer

 

   Dim value As Double

 

   For outerIndex = 0 To mOuter

     For innerIndex = 0 To mInner

       ´ tutaj wykonywane będą jakieś fajne obliczenia

       value = Math.Sqrt(CDbl(innerIndex - outerIndex))

     Next

   Next

 End Sub

End Class

Ta klasa została zaprojektowana, aby działać w wątku w tle – można ją uruchomić za pomocą poniższego kodu:

Dim myWorker As New Worker(10000000, 10)

Dim backThread As New Thread(AddressOf myWorker.Work)

backThread.Start()

Klasa Worker (Robotnik) do przechowywania swoich danych wykorzystuje własne zmienne instancji. Te zmienne, mInner oraz mOuter mogą być bezpiecznie wykorzystywane przez wątek działający w tle przy uzasadnionym założeniu, że nie zostaną wykorzystane w tym samym czasie przez żaden inny wątek.

Klasa ta posiada konstruktor, który można wykorzystać do zainicjowania obiektu dowolnymi danymi początkowymi. Przed właściwym uruchomieniem wątku w tle główny kod aplikacji tworzy instancję obiektu i inicjuje ją za pomocą danych, na których ten wątek działać.

Wątek działający w tle otrzymuje adres metody Work (Praca) obiektu i wtedy zostaje uruchomiony – będzie teraz wykonywał kod wewnątrz obiektu, działając na danych właściwych dla tego obiektu.

Ponieważ obiekt jest samowystarczalny, możliwe jest stworzenie wielu takich obiektów, z których każdy będzie działał we własnym wątku – odizolowany od innych.

Taka implementacja nie jest jednak idealna, gdyż interfejs użytkownika nie może poznać stanu procesu działającego w tle. Nie został też zaimplementowany żaden mechanizm, za pomocą którego interfejs użytkownika mógłby zażądać przerwania wykonywania procesu działającego w tle.

Taka sytuacja wymaga zainicjowania jakiejś interakcji między procesem w tle a procesem interfejsu użytkownika. To właśnie brak tej interakcji komplikuje wszystko, więc byłoby miło, gdyby można było abstrahować tę współzależność w klasie tak, aby ani interfejs użytkownika, ani kod roboczy nie musiały „troszczyć się” o szczegóły.

Architektura

Możliwe jest stworzenie architektury, która będzie separować interfejs użytkownika oraz wątek roboczy (wykonujący właściwe operacje) w taki sposób, by nie było niepożądanych interakcji. Można to nawet napisać w uniwersalny sposób – by schemat mógł być zastosowany w dowolnych aplikacja Windows Forms.

Najpierw omówimy architekturę, potem przejdziemy do projektowania oraz implementowania kodu. Plik do pobrania do niniejszego artykułu zawiera przykładową aplikację pokazującą sposób jej wykorzystania.

Zwykle aplikacja rozpoczyna się pojedynczym wątkiem otwierającym interfejs użytkownika. Nazwijmy go dla uproszczenia wątkiem IU (Interfejsu Użytkownika). W wielu aplikacjach będzie to jedyny wątek i tam on zajmuje się interfejsem użytkownika oraz całym przetwarzaniem.

Jednakże w naszym przypadku utworzony zostanie wątek roboczy, który będzie wykonywał jakieś czynności w tle, pozwalając wątkowi IU na „skupienie się” na interfejsie użytkownika, dzięki czemu będzie on reagował na działania użytkownika nawet wtedy, gdy wątek roboczy jest bardzo zajęty (wykorzystywany???).

Pomiędzy wątkiem IU oraz wątkiem roboczym wstawimy warstwę kodu, która będzie działać jako interfejs pomiędzy interfejsem użytkownika a kodem roboczym. Ten kod jest zasadniczo Kontrolerem, zarządzającym oraz kontrolującym wątek roboczy oraz jego interakcję z interfejsem użytkownika.

Rysunek 1. Wątek IU, kontroler oraz wątek roboczy

Kontroler będzie zawierał cały kod niezbędny do bezpiecznego uruchomienia wątku roboczego, przekazywania różnych komunikatów statusu z wątku roboczego z powrotem do wątku IU oraz wszelkich żądań anulowania z wątku IU do wątku roboczego. Kot interfejsu użytkownika oraz kod roboczy nie wchodzą w bezpośrednie interakcje; komunikacja zawsze przechodzi teraz przez kod kontrolera.

Dzieje się tak zawsze, z wyjątkiem przed rozpoczęciem oraz po zakończeniu aktywności wątku roboczego – wtedy kod interfejsu użytkownika może wchodzić w bezpośrednie interakcje z obiektem Worker. Zanim wątek roboczy zostanie uruchomiony, interfejs użytkownika może utworzyć oraz zainicjować obiekt Worker. Po zakończeniu działania wątku roboczego interfejs użytkownika może pobrać dowolne wartości z obiektu Worker. Z perspektywy interfejsu użytkownika powstaje następujący ciąg zdarzeń:

  1. Utworzenie obiektu Worker.

  2. Zainicjowanie obiektu Worker.

  3. Wywołanie Kontrolera do rozpoczęcia wątku roboczego:

  4. obiekt Worker może wysyłać informacje o statusie do interfejsu użytkownika poprzez Kontrolera,

  5. interfejs użytkownika może wysyłać żądanie anulowania do obiektu Worker poprzez Kontrolera.

  6. Obiekt Worker informuje interfejs użytkownika o zakończeniu pracy poprzez Kontrolera.

  7. Wartości można pobierać bezpośrednio z obiektu Worker.

Poza ograniczeniem mówiącym, że kod interfejsu użytkownika nie może wchodzić w bezpośrednie interakcje z obiektem Worker, dopóki jest on aktywny, nie ma żadnych innych wymagań dotyczących kodu interfejsu użytkownika. Interfejs użytkownika pozostaje aktywny i reaguje na działania użytkownika nawet podczas działania w tle procesu roboczego.

Z perspektywy obiektu Worker zachodzi następujący ciąg zdarzeń:

  1. Kod interfejsu użytkownika generuje obiekt Worker.

  2. Kod interfejsu użytkownika inicjuje obiekt Worker przy użyciu wszelkich niezbędnych danych.

  3. Kontroler tworzy wątek działający w tle i wywołuje metodę obiektu Worker:

  4. obiekt Worker uruchamia kod roboczy,

  5. obiekt Worker przekazuje wszelkie informacje na temat statusu do Kontrolera, aby ten mógł je przekazać do interfejsu użytkownika,

  6. gdy jest to możliwe, obiekt Worker sprawdza, czy wysłano jakieś żądanie anulowania, a jeśli tak – przerywa działanie,

  7. po zakończeniu pracy obiekt Worker informuje o tym obiekt Controller (Kontroler), żeby ten mógł poinformować o tym interfejs użytkownika.

  8. Po zakończeniu działania wątku roboczego interfejs użytkownika może wchodzić w bezpośrednie interakcje z obiektem Worker.

Ponieważ kod roboczy wchodzi w interakcje tylko z Kontrolerem, nie trzeba się martwić o przypadkowe wpłynięcie „wątku roboczego” na komponenty interfejsu użytkownika, co niemalże na pewno zdestabilizowałoby aplikację. Zamiast tego kod roboczy wykorzystuje kontroler by komunikować się z z wątkiem interfejsu użytkownika, więc wszystko jest bezpieczne.

Ostatecznie oznacza to, że nie trzeba też martwić się podczas pisania kodu żadnymi kwestiami dotyczącymi  wątków, dopóki wykorzystywane będą tylko zmienne instancji obiektu Worker.

Dla wyjaśnienia interakcji pomiędzy różnymi komponentami, zwłaszcza takimi w różnych wątkach, często najlepsze jest użycie diagramów. Microsoft® Visio® obsługuje tworzenie diagramów Języka Modelowania Uniwersalnego UML (ang. Universal Modeling Language), które mogą być bardzo pomocne.

W poniższym diagramie sekwencji UML zilustrowano przepływ zdarzeń między interfejsem użytkownika, obiektem Worker oraz Kontrolerem. Przyjęto, że nie wpłyną komunikat anulowania. Na pionowym pasku pod obiektami Worker oraz Controller (Kontroler) pokazany jest kod uruchomiony w wątku roboczym. Cała reszta kodu działa w wątku interfejsu użytkownika.

Rysunek 2. Diagram sekwencji ilustrujący cały proces

Inne spojrzeniem na architekturę daje diagramu aktywności UML. Ten styl diagramu skupia się raczej na zadaniach niż na obiektach, więc pokazuje serię wykonywanych kroków oraz działanie procesu krok-po-kroku. Łatwo można zauważyć, że kod interfejsu użytkownika pozostaje w jednym wątku (po lewej), podczas gdy obiekt Worker wykonuje swoją pracę w drugim wątku (po prawej). Przed rozpoczęciem oraz po zakończeniu jego działania w tym drugim wątku, obiekt Worker może być bezpośrednio wykorzystywany przez interfejs użytkownika do zainicjowania wartości początkowych oraz do pobrania wyników działania.

Rysunek 3. Diagram aktywności pokazujący cały proces

Użycie takich jak ten diagramów może pomóc w zlokalizowaniu obszarów,  gdzie interfejs użytkownika omyłkowo wchodzi w bezpośrednie interakcje z procesem roboczym (lub odwrotnie), podczas gdy wątek działający w tle pozostaje aktywny. Wszelkie takie interakcje wymagają napisania dodatkowego kodu zapobiegającego błędom mogącym zdestabilizować aplikację. W idealnej sytuacji taka interakcja zachodzi za pośrednictwem Kontrolera, gdzie można zawrzeć cały kod dbający o bezpieczeństwo interakcji pomiędzy procesami.

Poniższy rysunek ilustruje sekwencję zdarzeń w przypadku wysłania przez interfejs użytkownika żądania anulowania.

Rysunek 4. Diagram sekwencji pokazujący żądanie anulowania

Należy zauważyć, że żądanie anulowania jest wysyłane z interfejsu użytkownika do Kontrolera i to do Robotnika należy sprawdzenie w Kontrolerze, czy pojawiło się żądanie anulowania. Ani interfejs użytkownika, ani Kontroler nie wymuszają przerwania działania kodu roboczego, co pozwala temu kodowi na bezproblemowe i bezpieczne zakończenie pracy na jego własnych warunkach.

Projekt infrastruktury

W celu zaimplementowania omówionego zachowania oczywiście konieczne jest zaimplementowanie klasy Controller (Kontroler). Aby ta infrastruktura była użyteczna w przypadku wielu różnych scenariuszy, zdefiniujemy także kilka formalnych interfejsów, z których może skorzystać Kontroler w przypadku interakcji z interfejsem użytkownika (lub klientem) oraz „robotnikiem”.

Po zdefiniowaniu formalnych interfejsów dla klienta oraz robotnika możliwe jest wykorzystanie tego samego Kontrolera w wielu różnych sytuacjach, przy różnych wymaganiach ze strony interfejsu użytkownika oraz dla różnych obiektów Worker.

Poniższy diagram UML klasy pokazuje klasę Controller (Kontroler) z interfejsami IClient oraz IWorker. Pokazuje także interfejs IController, który może być używany przez każdy kod roboczy do interakcji z obiektem Controller.

Rysunek 5. Diagram klas Kontrolera i związanych z nim interfejsów

Interfejs IClient definiuje metody, które będą wywoływane przez obiekt Controller w celu zawiadomienia klienta-interfejsu użytkownika o rozpoczęciu i zakończeniu działania obiektu Worker oraz do informowania klienta-interfejsu użytkownika o jakichkolwiek pojawiających się w tym przedziale informacjach o statusie. Zawiera także metodę służącą do sygnalizowania niepowodzeń podczas wykonywania kodu roboczego.

W wielu przypadkach możliwe jest zdefiniowanie tych metod jako zdarzeń zgłaszanych przez obiekt Controller i obsługiwanych przez interfejs użytkownika. Nie ma jednak łatwego sposobu na zgłoszenie zdarzenia z wewnątrz wątku roboczego w taki sposób, by zostało ono poprawnie obsłużone przez wątek interfejsu użytkownika, z zatem procedury te zostaną zaimplementowane jako zbiór metod.

Względnie proste natomiast jest sprawienie, żeby kod kontrolera działający w wątku roboczym wywoływał te metody w interfejsie użytkownika, tak aby zostały one obsłużone przez wątek interfejsu użytkownika.

Podobnie interfejs IWorker definiuje metody, które będą wywoływane przez obiekt Controller w celu interakcji z kodem roboczym. W celu dostarczenia kodu roboczego odwołania do obiektu Controller używana jest metoda Initialize (Inicjacja), natomiast metoda Start – do uruchomienia przetwarzania w wątku działającym w tle.

Metoda Start nie może mieć żadnych parametrów, co jest spowodowane sposobem działania wątków – podczas uruchamiania nowemu wątkowi przekazywany jest adres metody niepobierającej żadnych parametrów.

Ponownie należy zwrócić uwagę na to, że nie ma żadnej metody Cancel (Anuluj) ani Stop będącej częścią interfejsu IWorker; zamiast tego kod roboczy może użyć interfejsu IController do zapytania obiektu Controller o zażądanie anulowania operacji.

Interfejs IController definiuje metody, które kod roboczy może wywoływać dla obiektu Controller i sprawdzić flagę Running (Uruchomiony)  przyjmującą wartość False (Fałsz) przy żądaniu anulowania. Kod roboczy może również poinformować o zakończeniu lub przerwaniu pracy kontrolera (Controller), a także przesłać mu uaktualnione informacje o statusie oraz stopniu wykonania zadania (wartość typu Integer o wartości od 0 do 100).

Na koniec zdefiniowaliśmy sam obiekt Controller. Posiada on kilka metod, które mogą być wywoływane przez kod interfejsu użytkownika. Jest to między innymi metoda Start, która uruchamia przetwarzanie w tle, dostarczając obiektowi Controller wskazanie na obiekt Worker. Jest to także metoda Cancel (Anuluj), która jest używana do wysyłania żądań anulowania operacji. Interfejs użytkownika może także odczytać właściwość Running w celu sprawdzenia, czy zażądano anulowania, oraz właściwość Percent (Procenty) do sprawdzenia, jaki procent zadania został już wykonany.

Klasa Controller zawiera metodę constructor przyjmującą jako parametr IClient, umożliwiając interfejsowi użytkownika dostarczenie obiektowi Controller wskazania na formatkę, który będzie przetwarzał i wyświetlał wszelkie komunikaty od obiektu Worker.

Aby zaimplementować animowaną serię kropek wskazującą aktywność, utworzymy prostą kontrolkę formatek Windows używającą stopera do zmieniania kolorów w serii kontrolek PictureBox.

Implementacja

Zaimplementujemy tę infrastrukturę w projekcie Biblioteki Klas (ang. Class Library), tak aby mogła ona zostać użyta przez każdą aplikację wymagającą uruchamiania procesu w tle.

W tym celu należy otworzyć Visual Studio .NET i utworzyć nową aplikację Biblioteki Klas o nazwie Background (tło). Ponieważ ta biblioteka będzie zawierać kontrolki oraz formatki Windows, potrzebne będzie odwołanie do System.Windows.Forms.dll oraz System.Windows.Drawing.dll przy użyciu dialogu Add References (dodaj Referencje). Dodatkowo można zaimportować te przestrzenie nazw dla całego projektu za pomocą dialogu właściwości projektu pokazanego na Rysunku 6.

Rysunek 6. Użycie właściwości projektu w celu zaimportowania przestrzeni nazw dla całego projektu

Po wykonaniu tego kroku, można już zacząć pisanie kodu. Zacznijmy od utworzenia naszych interfejsów.

Definiowanie interfejsów

Dodajmy do projektu klasę o nazwie IClient i zastąpmy jej kod następującym:

Public Interface IClient

 Sub Start(ByVal Controller As Controller)

 Sub Display(ByVal Text As String)

 Sub Failed(ByVal e As Exception)

 Sub Completed(ByVal Cancelled As Boolean)

End Interface

Następnie dodajmy klasę o nazwie IWorker i zastąpmy jej kod następującym:

Public Interface IWorker

 Sub Initialize(ByVal Controller As IController)

 Sub Start()

End Interface

Na koniec należy dodać klasę o nazwie IController zawierającą poniższy kod:

Public Interface IController

 ReadOnly Property Running() As Boolean

 Sub Display(ByVal Text As String)

 Sub SetPercent(ByVal Percent As Integer)

 Sub Failed(ByVal e As Exception)

 Sub Completed(ByVal Cancelled As Boolean)

End Interface

W tym momencie mamy już zdefiniowane wszystkie interfejsy z diagramu klas omówionego wcześniej w niniejszym artykule – możemy zająć się implementacją klasy Controller.

Klasa Controller (Kontroler)

Teraz jesteśmy już gotowi do zaimplementowania rdzenia tej infrastruktury – klasy Controller, która będzie zawierała kod uruchamiający wątek roboczy oraz pośredniczący pomiędzy interfejsem użytkownika oraz wątkami roboczymi aż do zakończenia działania pracy wątku roboczego.

Dodajmy do projektu nową klasę o nazwie Controller. Najpierw jednak dodamy informacje o używanych przestrzeniach nazw i zadeklarujemy kilka zmiennych:

Imports System.Threading

 

Public Class Controller

 Implements IController

 

 Private mWorker As IWorker

 Private mClient As Form

 Private mRunning As Boolean

 Private mPercent As Integer

Teraz musimy zadeklarować kilka delegatów. Delegat to formalny wskaźnik wskazujący na metodę – delegat dla metody musi mieć taką samą sygnaturę metody (typy parametrów itd.) jak sama metoda.

Delegatów używa się do wielu rzeczy. W naszym przypadku delegaty są ważne, ponieważ umożliwiają wywoływanie przez jeden wątek metody, tak aby została ona uruchomiona w wątku interfejsu użytkownika formatki. Konieczne jest ustalenie delegacji dla wszystkich trzech metod, które będą wywoływane z formatki – zgodnie z definicją interfejsu IClient:

 ´ ta sygnatura delegacji odpowiada sygnaturze

 ´ IClient.Completed i jest używana do bezpiecznego

 ´ wywoływania tej metody w wątku interfejsu użytkownika

 Private Delegate Sub CompletedDelegate(ByVal Cancelled As Boolean)

 

 ´ ta sygnatura delegacji odpowiada sygnaturze

 ´ IClient.Display i jest używana do bezpiecznego

 ´ wywoływania tej metody w wątku interfejsu użytkownika

 Private Delegate Sub DisplayDelegate(ByVal Text As String)

 

 ´ ta sygnatura delegacji odpowiada sygnaturze

 ´ IClient.Failed i jest używana do bezpiecznego

 ´ wywoływania tej metody w wątku interfejsu użytkownika

 Private Delegate Sub FailedDelegate(ByVal e As Exception)

IClient definiuje także metodę Start, ale ta metoda będzie wywoływana z samego wątku interfejsu użytkownika, więc delegowanie nie jest konieczne.

 Piszemy następnie kod wywoływany z wątku interfejsu użytkownika. Ten kod zawiera metodę-konstruktora, metody Start oraz Cancel (Anuluj), a także właściwość Percent (Procenty). Umieszczone są one w regionie (Region) dla  jasności, że są wywoływane w wątku interfejsu użytkownika.

#Region " Kod wywoływany z wątku interfejsu użytkownika "

 ´ zainicjowanie kontrolera przy użyciu klienta

 Public Sub New(ByVal Client As IClient)

   mClient = CType(Client, Form)

 End Sub

 

 ´ ta metoda jest uruchamiana przez interfejs użytkownika

 ´ więc działa w wątku interfejsu użytkownika. W tym miejscu

 ´ uruchamia się wątek roboczy

 Public Sub Start(Optional ByVal Worker As IWorker = Nothing)

   ´ jeśli jest już uruchomiony, zgłoszony zostanie błąd

   If mRunning Then

     Throw New Exception("Proces już działa w tle ")

   End If

 

   mRunning = True

 

   ´ przechowanie odwołania do obiektu-robotnika

   ´ i zainicjowanie obiektu-robotnika, tak aby

   ´ miał odwołanie do kontrolera (Controller)

   mWorker = Worker

   mWorker.Initialize(Me)

 

   ´ teraz tworzony jest wątek działający w tle

   ´ do wykonania przetwarzania w tle

   Dim backThread As New Thread(AddressOf mWorker.Start)

 

   ´ teraz rozpoczyna się praca w tle

   backThread.Start()

 

   ´ poinformowanie klienta, że proces w tle wystartował

   CType(mClient, IClient).Start(Me)

 End Sub

 

 ´ ta metoda jest uruchamiana przez interfejs użytkownika

 ´ więc działa w wątku interfejsu użytkownika. Tylko

 ´ ustawia flagę żądania anulowania

 Public Sub Cancel()

   mRunning = False

 End Sub

 

 ´ zwraca wartość oznaczającą stopień wykonania zadania

 ´ (w procentach) i jest wywoływana tylko przez wątek

 ´ interfejsu użytkownika

 Public ReadOnly Property Percent() As Integer

   Get

     Return mPercent

   End Get

 End Property

 

#End Region

Jedyny specjalny fragment kodu znajduje się w metodzie Start, w której wątek roboczy jest tworzony, a następnie uruchamiany:

   Dim backThread As New Thread(AddressOf mWorker.Start)

 

   backThread.Start()

W celu utworzenia wątku przekazywany jest adres metody Start interfejsu IWorker obiektu Worker. Potem po prostu wywoływana jest metoda Start obiektu wątkudo rozpoczęcia przetwarzania. W tym miejscu należy zachować ostrożność, aby interfejs użytkownika nie wchodził w bezpośrednie interakcje z obiektem Worker, ani obiekt Worker nie wchodził w bezpośrednie interakcje z interfejsem użytkownika.

Należy zauważyć, że metoda Cancel (Anuluj) tylko ustawia flagę do zasygnalizowania żądania zakończenia działania. To do kodu roboczego należy okresowe sprawdzanie tej flagi w celu stwierdzenia, czy powinien już zakończyć działanie.

Teraz możemy zaimplementować kod, który będzie wywoływany podczas działania obiektu Worker. Ten kod jest bardziej interesujący, ponieważ musi przekazywać wywołania zarówno metody Display (Wyświetl), jak i metody Completed (Zakońcozno) z wątku roboczego do interfejsu użytkownika, ale w wątku interfejsu użytkownika.

W tym celu użyta zostanie metoda Invoke (Wywołaj) obiektu-formatki. Ta metoda przyjmuje wskaźnik delegata, którego formatka powinna wywołać, wraz z tabelą typu Object zawierającą parametry dla tej metody.

Metoda Invoke nie wywołuje metody w formie bezpośrednio form. Wymaga natomiast od formularz zawrócenia i wywołania tej metody przy użyciu wątku interfejsu użytkownika formularza. Dzieje się tak niejawnie – poprzez wysłanie komunikatu systemu Windows do formularza. Oznacza to, że forma otrzymuje te wywołania metod mniej więcej tak samo, jak otrzymuje od systemu operacyjnego zdarzenia click (kliknięcie) oraz keypress (naciśnięcie klawisza).

Szczegóły nie są tu istotne, gdyż wynik jest taki, że metoda Invoke wyzwala proces, w wyniku którego formatka uruchamia metodę w wątku interfejsu użytkownika, o co właśnie chodziło.

Poniższy kod ponownie zostaje umieszczony wewnątrz regionu, aby było absolutnie jasne, że jest on wywoływany w wątku roboczym:

#Region " Kod wywoływany z wątku roboczego "

 ´ wywoływana z wątku roboczego w celu uaktualnienia ekranu

 ´ to wyzwala wywołanie metody do interfejsu użytkownika z

 ´ tekstem statusu – i to wywołanie zachodzi w wątku

 ´ interfejsu użytkownika

 Private Sub Display(ByVal Text As String) _

     Implements IController.Display

   Dim disp As New DisplayDelegate( _

     AddressOf CType(mClient, IClient).Display)

   Dim ar() As Object = {Text}

 

   ´ wywołanie klienta z wątku interfejsu użytkownika

   ´ w celu uaktualnienia ekranu

   mClient.BeginInvoke(disp, ar)

 

 End Sub

 

 ´ wywoływana z wątku roboczego w celu zasygnalizowania awarii

 ´ to wyzwala wywołanie metody do interfejsu użytkownika z

 ´ tekstem statusu – i to wywołanie zachodzi w wątku

 ´ interfejsu użytkownika

 Private Sub Failed(ByVal e As Exception) _

     Implements IController.Failed

 

   Dim disp As New FailedDelegate(_

     AddressOf CType(mClient, IClient).Failed)

   Dim ar() As Object = {e}

 

   ´ wywołanie klienta z wątku interfejsu użytkownika

   ´ w celu zasygnalizowania awarii

   mClient.Invoke(disp, ar)

 

 End Sub

 

 ´ wywoływana z wątku roboczego w celu poinformowania o stopniu ukończenia

 ´ ta wartość jest przekazywana do kontrolera, gdzie może w każdej chwili

 ´ zostać odczytana przez interfejs użytkownika

 Private Sub SetPercent(ByVal Percent As Integer) _

     Implements IController.SetPercent

 

   mPercent = Percent

 

 End Sub

 

 ´ wywoływana z wątku roboczego w celu zasygnalizowania zakończenia

 ´ przekazywany jest także parametr sygnalizujący, czy

 ´ proces został normalnie zakończony, czy też został anulowany

 ´ wywołanie interfejsu użytkownika ma miejsce w wątku interfejsu użytkownika

 Private Sub Completed(ByVal Cancelled As Boolean) _

     Implements IController.Completed

 

   mRunning = False

   Dim comp As New CompletedDelegate( _

     AddressOf CType(mClient, IClient).Completed)

   Dim ar() As Object = {Cancelled}

 

   ´ wywołanie formatki klienta w wątku interfejsu użytkownika

   ´ w celu zasygnalizowania zakończenia

   mClient.Invoke(comp, ar)

 

 End Sub

 

 ´ sygnalizuje, czy proces nadal działa, czy też

 ´ wysłano żądanie anulowania

 ´ ta metoda jest wywoływana w wątku roboczym, aby

 ´ kod roboczy wiedział, czy powinien zakończyć

 ´ swoje działanie

 Private ReadOnly Property Running() As Boolean _

     Implements IController.Running

   Get

     Return mRunning

   End Get

 End Property

 

#End Region

Metody Failed (niepowodzenie) oraz Completed (ukończono) wykorzystują metodę Invoke formatki. Metoda Failed wykonuje co następuje:

   Dim disp As New FailedDelegate(_

     AddressOf CType(mClient, IClient).Failed)

   Dim ar() As Object = {e}

 

   ´ wywołanie formatki klienta w wątku interfejsu użytkownika

   ´ w celu zasygnalizowania niepowodzenia

   mClient.Invoke(disp, ar)

Najpierw tworzymy delegata wskazującego metodę Failed formatki klienta z interfejsu IClient. Następnie deklarujemy tablicę danych typu object zawierającą wartości parametrów przekazywanych do tej metody. Na koniec wywołujemy metodę Invoke formularza klienta, przekazując wskaźnik delegata oraz tablicę parametrów do formularza.

Następnie formularz wywołuje tę metodę z tymi parametrami w wątku interfejsu użytkownika, gdzie może bezpiecznie działać  i uaktualnić ekran.

Cały ten proces jest synchroniczny, co oznacza, że wątek roboczy jest blokowany na czas wywołania metody formularza. O ile blokowanie wątku roboczego dla komunikatu błędu lub komunikatu o zakończeniu działania jest do przyjęcia, blokowanie go do wyświetlenia każdej najmniejszej zmiany statusu nie jest raczej pożądane.

Aby uniknąć blokowania podczas wyświetlania statusu, metoda Display (wyświetl) używa BeginInvoke (Rozpocznij Wywołanie) zamiast Invoke (Wywołaj). BeginInvoke powoduje asynchroniczne wywołanie metody formatki, dzięki czemu wątek roboczy nie przestaje działać i nie czeka na zakończenie działania metody wyświetlającej formatkę:

   Dim disp As New DisplayDelegate( _

     AddressOf CType(mClient, IClient).Display)

   Dim ar() As Object = {Text}

 

   ´ wywołanie formatki klienta dla wątku interfejsu użytkownika

   ´ w celu uaktualnienia ekranu

   mClient.BeginInvoke(disp, ar)

Użycie BeginInvoke w ten właśnie sposób pomaga utrzymać maksymalną możliwą wydajność wątku roboczego poprzez unikanie blokowania.

Kontrolka ActivityBar (pasek aktywności)

Na koniec tworzymy kontrolkę ActivityBar wyświetlającą animowane kropki i dodajmy Kontrolkę Użytkownika (User Control) do projektu o nazwie ActivityBar oraz zmieniamy jej rozmiar na 110 (szerokość) na 20 (wysokość). Można to zrobić przeciągając jej granice lub ustawiając jej właściwość Size (Rozmiar) w oknie właściwości (Properties).

Całą resztę zrobimy w kodzie. Do stworzenia animowanej serii „świateł” mrugających na ekranie użyjemy serii kontrolek PictureBox z kontrolką Timer (Stoper). Przy każdym wyzwoleniu kontrolki Timer następna kontrolka PictureBox stanie się zielona, a ta, która była zielona, otrzyma kolor tła formatki.

Umieszczamy kontrolkę Timer z paska narzędziowego (ang. Toolbox) w formatce i zmieniamy jej nazwę na tmAnim. Aby uzyskać właściwą prędkość animacji, należy ustawić właściwość Interval (Interwał) na 300.

Przy okazji: jest jeszcze jedna kontrolka Timer – znajduje się ona na zakładce Components (komponenty). Jest to stoper wielowątkowy, który wywołuje zdarzenie Elapsed (czas minął) dla wątku działającego w tle, a nie dla wątku interfejsu użytkownika, jak stoper z formatki Windows. Nie ma to jednak znaczenia przy tworzeniu interfejsu użytkownika, ponieważ kod w zdarzeniu Elapsed oczywiście nie może wchodzić w bezpośrednie interakcje z interfejsem użytkownika.

Teraz dodajmy do kontrolki następujący kod:

 Private mBoxes As New ArrayList()

 Private mCount As Integer

 

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

   ByVal e As System.EventArgs) Handles MyBase.Load

 

   Dim index As Integer

 

   If mBoxes.Count = 0 Then

     For index = 0 To 6

       mBoxes.Add(CreateBox(index))

     Next

   End If

   mCount = 0

 

 End Sub

 

 Private Function CreateBox(ByVal index As Integer) As PictureBox

   Dim box As New PictureBox()

 

   With box

     SetPosition(box, index)

     .BorderStyle = BorderStyle.Fixed3D

     .Parent = Me

     .Visible = True

   End With

   Return box

 End Function

 

 Private Sub GrayDisplay()

   Dim index As Integer

 

   For index = 0 To 6

     CType(mBoxes(index), PictureBox).BackColor = Me.BackColor

   Next

 End Sub

 

 Private Sub SetPosition(ByVal Box As PictureBox, ByVal Index As Integer)

   Dim left As Integer = CInt(Me.Width / 2 - 7 * 14 / 2)

   Dim top As Integer = CInt(Me.Height / 2 - 5)

 

   With Box

     .Height = 10

     .Width = 10

     .Top = top

     .Left = left + Index * 14

   End With

 End Sub

 

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

   ByVal e As System.EventArgs) Handles tmAnim.Tick

 

   CType(mBoxes((mCount + 1) Mod 7), PictureBox).BackColor = _

     Color.LightGreen

   CType(mBoxes(mCount Mod 7), PictureBox).BackColor = Me.BackColor

 

   mCount += 1

   If mCount > 6 Then mCount = 0

 End Sub

 

 Public Sub Start()

   CType(mBoxes(0), PictureBox).BackColor = Color.LightGreen

   tmAnim.Enabled = True

 End Sub

 

 Public Sub [Stop]()

   tmAnim.Enabled = False

   GrayDisplay()

 End Sub

 

 Private Sub ActivityBar_Resize(ByVal sender As Object, _

   ByVal e As System.EventArgs) Handles MyBase.Resize

 

   Dim index As Integer

 

   For index = 0 To mBoxes.Count - 1

     SetPosition(CType(mBoxes(index), PictureBox), index)

   Next

 End Sub

Zdarzenie Load (załaduj) formatki tworzy kontrolki PictureBox i umieszcza je w tablicy tak, aby łatwo można było się między nimi przemieszczać. Zdarzenie Tick kontrolki Timer sprawia, że po kolei każda z kontrolek PictureBox staje się zielona.

Wszystko to jest rozpoczynane przez metodę Start i zatrzymywane przez zdarzenie Stop. Ponieważ Stop jest słowem zastrzeżonym, nazwa tej metody jest zamknięta w nawiasie kwadratowym [Stop]. Metoda Stop nie tylko zatrzymuje stoper, ale także zmienia kolor wszystkich kontrolek PictureBox na szary, aby pokazać użytkownikowi, że w dane chwili nie ma żadnej aktywności.

Tworzenie klasy Worker

Wcześniej w niniejszym artykule została pokazana prosta klasa Worker. Po zdefiniowaniu interfejsu IWorker możemy ją teraz rozbudować i wykorzystać utworzonego w niej kontrolera.

Najpierw jednak musimy utworzyć plik Background.dll. Jest to ważne, ponieważ bez wykonania tego kroku kontrolka ActivityBar nie pojawi się w Oknie Narzędzi podczas tworzenia testowej formatki.

W tym celu należy do zestawu dodać projekt Windows Forms i nazwać go bgTest. Wybierając go na projekt startowy należy kliknąć na nim prawym przyciskiem myszy w Solution Explorer i wybrać stosowną pozycję w menu.

Następnie należy użyć zakładki Projects (Projekty) dialogu Add References (dodaj referencje) w celu dodania odwołania do projektu Background.

Teraz trzeba dodać do projektu klasę o nazwie Worker (Robotnik). Będzie ona zawierała wcześniejszy kod, ale także dodatkowy kod implementujący interfejs IWorker (na poniższym wydruku te fragmenty są wyróżnione):

Imports Background

 

Public Class Worker

 Implements IWorker

 

 Private mController As IController

 Private mInner As Integer

 Private mOuter As Integer

 

 Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer)

   mInner = InnerSize

   mOuter = OuterSize

 End Sub

 

 ´ wywoływana przez kontrolera, tak abyśmy mogli uzyskać

 ´ odwołanie do kontrolera

 Private Sub Init(ByVal Controller As IController) _

     Implements IWorker.Initialize

 

   mController = Controller

 

 End Sub

 

 Private Sub Work() Implements IWorker.Start

   Dim innerIndex As Integer

   Dim outerIndex As Integer

 

   Dim value As Double

 

   Try

     For outerIndex = 0 To mOuter

       If mController.Running Then

         mController.Display("Zewnętrzna pętla " & outerIndex & " rozpoczyna się")

         mController.SetPercent(CInt(outerIndex / mOuter * 100))

 

       Else

         ´ zażądano anulowania

         mController.Completed(True)

         Exit Sub

       End If

 

       For innerIndex = 0 To mInner

         ´ tutaj wykonywane będą jakieś fajne obliczenia :)

         value = Math.Sqrt(CDbl(innerIndex - outerIndex))

       Next

     Next

     mController.SetPercent(100)

     mController.Completed(False)

 

   Catch e As Exception

     mController.Failed(e)

 

   End Try

 End Sub

End Class

Dodaliśmy metodę Init implementującą IWorker.Initialize. Controller wywołuje tę metodę do późniejszego odwołania do obiektu Controller.

Zmieniliśmy także metodę Work tak, żeby była prywatna (Private) oraz żeby implementowała metodę IWorker.Start. Zostanie ona uruchomiona w wątku roboczym.

Metoda Work została rozbudowana tak, aby wykorzystywała blok Try..Catch, dzięki czemu możliwe jest wychwycenie wszystkich błędów i zwrócenie ich do interfejsu użytkownika za pomocą metody Failed obiektu Controller.

Zakładając, że nasz kod jest uruchomiony, wywołujemy metody Display oraz SetPercent obiektu Controller w celu uaktualnienia podczas działania kodu statusu oraz stopnia wykonania zadania.

Możemy także okresowo odczytywać właściwość Running obiektu Controller do sprawdzenia, czy nie zażądano anulowania. Jeśli zażądano, proces zostaje zatrzymany oraz zasygnalizowane zostanie zakończenie działania na skutek wystąpienia żądania anulowania.

Tworzenie formularza do wyświetlania

Możemy teraz zająć się stworzeniem formularza służącego do uruchamiania oraz ewentualnie zatrzymywania procesów działających w tle. Formularz ten będzie także wyświetlał informacje o aktywności oraz statusie.

W tym celu należy otworzyć projektanta dla formularza Form1 i dodać do niego dwa przyciski (btnStart oraz btnRequestCancel), dwie etykiety (Label1 oraz Label2), pasek postępu (ProgressBar1) oraz nasz pasek aktywności (ActivityBar1) – jak pokazano na Rysunku 7.

Rysunek 7. Układ kontrolek formularza Form1

Formatka musi zaimplementować IClient, aby obiekt Controller mógł wchodzić z nim w interakcje:

Imports Background

 

Public Class Form1

 Inherits System.Windows.Forms.Form

 

 Implements IClient

Formularz będzie także potrzebował obiektu Controller oraz flagi – dzięki temu będzie wiedział, czy przetwarzanie w tle jest aktywne czy zakończone.

 Private mController As New Controller(Me)

 Private mActive As Boolean

Teraz można już dodać metody implementujące interfejs zdefiniowany przez IClient. Warto umieścić te metody w regionie – dzięki temu wyraźnie widać, że implementują one wtórny interfejs:

#Region " IClient "

 

 Private Sub TaskStarted(ByVal Controller As Controller) _

     Implements IClient.Start

 

   mActive = True

   Label1.Text = "Startuję"

   Label2.Text = "0%"

   ProgressBar1.Value = 0

   ActivityBar1.Start()

 

 End Sub

 

 Private Sub TaskStatus(ByVal Text As String) _

     Implements IClient.Display

 

   Label1.Text = Text

   Label2.Text = CStr(mController.Percent) & "%"

   ProgressBar1.Value = mController.Percent

 

 End Sub

 

 Private Sub TaskFailed(ByVal e As Exception) _

     Implements IClient.Failed

 

   ActivityBar1.Stop()

   Label1.Text = e.Message

   MsgBox(e.ToString)

   mActive = False

 

 End Sub

 

 Private Sub TaskCompleted(ByVal Cancelled As Boolean) _

     Implements IClient.Completed

 

   Label1.Text = "Zakończono"

   Label2.Text = CStr(mController.Percent) & "%"

   ProgressBar1.Value = mController.Percent

   ActivityBar1.Stop()

   mActive = False

 

 End Sub

 

#End Region

Należy zauważyć, że żaden z tych fragmentów nie zawiera niczego, co jest związane z wątkami. Natomiast każdy z nich zawiera kod reagujący stosownie do otrzymywanych informacji o statusie procesu działającego w tle. W każdym przypadku ekran jest uaktualniany w celu uwidocznienia statusu procesu, stopnia wykonania (za pomocą tekstu lub paska postępu ProgressBar) oraz uruchomienia lub zatrzymania kontrolki ActivityBar.

Istotna jest flaga mActive. W przypadku zamknięcia przez użytkownika formularza lub gdy wątek roboczy jest jeszcze aktywny aplikacja może się zawiesić lub stać niestabilna. Aby się przed tym uchronić, można przechwycić zdarzenie Closing formatki i anulować te próby zamknięcia, póki proces nadal działa w tle.

 Private Sub Form1_Closing(ByVal sender As Object, _

   ByVal e As System.ComponentModel.CancelEventArgs) _

   Handles MyBase.Closing

 

   e.Cancel = mActive

 

 End Sub

Można także zainicjować w takim przypadku operację anulowania, ale to zależy od konkretnych założeń aplikacji.

Pozostało już tylko zaimplementowanie zdarzenia Click dla przycisków.

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

     ByVal e As System.EventArgs) Handles btnStart.Click

 

   mController.Start(New Worker(2000000, 100))

 

 End Sub

 

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

     ByVal e As System.EventArgs) Handles btnStop.Click

 

   Label1.Text = "Anulowanie ..."

   mController.Cancel()

 

 End Sub

Przycisk startu po prostu wywołuje metodę Start obiektu Controller przekazując jej instancję obiektu Worker.

Aby na konkretnej maszynie uzyskać odpowiednie wyniki, konieczne może być dostosowanie wartości użytych do zainicjowania obiektu Worker. Podane wyżej wartości są dobrym przykładem w komputerze z dwoma procesorami P3/450. Prawdziwy obiekt Worker zaimplementowałby znacznie bardziej sensowny, ale nadal długotrwały, proces.

Przycisk Cancel (Anuluj) wywołuje metodę Cancel obiektu Controller, a także uaktualnia ekran w celu zasygnalizowania, że wysłano żądanie anulowania. Należy pamiętać, że jest to tylko żądanie anulowania, więc zanim proces roboczy rzeczywiście się zatrzyma, może minąć trochę czasu. Pożądane jest natychmiastowe informowanie użytkownika przynajmniej o tym, że kliknięcie przyciskiem zostało zauważone.

Teraz można już uruchomić aplikację. Po kliknięciu przycisku Start, obiekt Worker powinien rozpocząć pracę, a ekran powinien się na bieżąco uaktualniać podczas działania. Możliwe jest przemieszczanie formularza po ekranie oraz inne rodzaje interakcje z tym formularzem, ponieważ wątek interfejsu użytkownika pozostaje w gruncie rzeczy bezczynny, więc cały czas jest dla użytkownika w stanie gotowości.

W tym samym czasie wątek roboczy wykonuje intensywne prace w tle, okresowo wysyłając do wyświetlenia uaktualnienia statusu do wątku interfejsu użytkownika.

Podsumowanie

Wielowątkowość jest potężnym narzędziem, które można wykorzystać zawsze, gdy mamy do wykonania jest jakieś długotrwałe zadanie. Umożliwia uruchomienia kodu roboczego bez blokowania interfejsu użytkownika. Ale - wielowątkowość może powodować niewiarygodne komplikacje – a na pewno utrudnia śledzenie.

Choć nie zawsze jest to możliwe, należy starać się zapewnić dla każdego wątku roboczego odizolowany zbiór danych, na których każdy z nich może działać. Najprostszym sposobem na to jest utworzenie dla każdego wątku osobnych obiektów zawierających dane oraz kod dla tego wątku.

Implementując taką infrastrukturę do pośredniczenia między wątkami roboczymi a wątkiem UI, można znacząco uprościć proces pisania wielowątkowego kodu roboczego oraz interfejsu pozwalającego kontrolować jego wykonanie. W niniejszym artykule pokazano taką infrastrukturę, którą można zaadaptować do potrzeb konkretnej aplikacji.

 

ROCKFORD LHOTKA

Magenic Technologies

1 października 2002 r.

tagi: Visual Basic

Podobne artykuły

Komentarze 0

pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.