Wielowątkowe C#

W ubiegłorocznym wydaniu PCWK na Gwiazdkę badaliśmy nietypowe zastosowania C#, tworząc aplikację w kształcie choinki, która miała kilka ciekawych "umiejętności". Dzisiaj rozwiniemy tamten program, dodając prostą animację - obsypiemy choinkę gwiezdnym pyłem, a przy okazji powiemy, czym są wątki i w jaki sposób można projektować aplikacje wielowątkowe w C#.

W ubiegłorocznym wydaniu PCWK na Gwiazdkę badaliśmy nietypowe zastosowania C#, tworząc aplikację w kształcie choinki, która miała kilka ciekawych "umiejętności". Dzisiaj rozwiniemy tamten program, dodając prostą animację - obsypiemy choinkę gwiezdnym pyłem, a przy okazji powiemy, czym są wątki i w jaki sposób można projektować aplikacje wielowątkowe w C#.

Czym są wątki i co to jest wielowątkowość? Wielowątkowość (multithreading) to cecha systemu operacyjnego, dzięki której w ramach jednego procesu (czyli uruchomionego programu) można wykonywać kilka wątków (thread). Wątki to niezależnie wykonywane ciągi instrukcji. Wszystkie wątki tego samego procesu mają dostęp do danych tego procesu. W C# oznacza to, że wątek może wykonywać jakąś metodę niezależnie od pozostałej części aplikacji.

Rysunek 1. Definicje użytych w artykule terminów informatycznych można znaleźć w encyklopediach internetowych, m.in.w wolnej encyklopedii Wikipedia ( http://pl.wikipedia.org ).

Rysunek 1. Definicje użytych w artykule terminów informatycznych można znaleźć w encyklopediach internetowych, m.in.w wolnej encyklopedii Wikipedia ( http://pl.wikipedia.org ).

W jakim celu? Chcę dodać do aplikacji opisywanej w poprzednim artykule animację. Załóżmy, że odpowiedzialne za tę animację instrukcje umieścimy w pętli wykonywanej w głównym wątku naszego procesu, a dokładniej w wątku związanym z formą. Dopóki główny wątek zaangażowany jest w wykonywanie instrukcji tej pętli, pozostałe czynności procesu zostaną zawieszone. W szczególności dostęp do menu kontekstowego czy nawet odświeżanie okna będzie wstrzymane do momentu jej zakończenia. A jeśli pętla ma działać bez przerwy? Wówczas działanie aplikacji zostanie kompletnie zablokowane. I właśnie do rozwiązania tego typu problemów służą wątki. Pętla wykonywana w osobnym wątku nie zacznie interferować z wątkiem obsługującym zdarzenia formy i okno aplikacji będzie działało bez zakłóceń.

Jak to zrobić? Biblioteka platformy .NET udostępnia dwie klasy do tworzenia dodatkowych wątków. Pierwszą jest komponent Timer. Drugą - klasa Thread. Pierwsza nadaje się raczej do czynności wykonywanych cyklicznie, co określony z góry czas. Druga pozwala na jednorazowe wykonanie ciągu instrukcji zapisanych w metodzie, bez ich powtarzania. Nie znaczy to jednak, że nie można komponentu Timer wykorzystać do jednorazowego uruchomienia jakiejś metody i że w metodzie wykonywanej w klasie Thread nie można umieścić pętli zawierającej polecenie wymuszające odczekanie wyznaczonego przez programistę czasu, do czego służy metoda Thread.Sleep, i w ten sposób imitować działania komponentu Timer.

Poniżej wykorzystamy obydwie drogi do wielowątkowości. Całą przygotowywaną przez nas animacją zarządzać będzie komponent Timer, który co ustalony przez nas czas będzie tworzył nową, rysowaną na choince gwiazdkę. Natomiast "życie" gwiazdki, czyli stopniowa zmiana jej barwy, będzie realizowana w obrębie wątku utworzonego za pomocą klasy Thread.

Ważnym elementem programowania wielowątkowego jest synchronizacja wątków. W naszym wypadku dodatkowe wątki związane z rysowaniem gwiazdek są od siebie zupełnie niezależne, wszystkie zależą jednak od wątku związanego z formą, którą wykorzystują do rysowania. Ważne jest zwłaszcza jak najszybsze wstrzymanie działania dodatkowych wątków w momencie, gdy forma jest zamykana, czyli gdy zgłoszone zostało zdarzenie Closed. Nie można dopuścić do tego, że wątek związany z formą zostanie już zamknięty, a dodatkowe wątki spróbują nadal coś w formie rysować. W tym celu do formy dodajemy prywatne pole typu logicznego o nazwie zamykanie, które inicjujemy wartością false:

private bool zamykanie = false;

Wartość tego pola będzie pozostawała false aż do chwili wywołania zdarzenia Closed. W jego metodzie zdarzeniowej umieścimy bowiem polecenie zmieniające wartość owego pola na true:

private void Form1_Closed(

object sender, System.EventArgs e)

{

zamykanie = true;

zapiszOpcje();

for (; Opacity>0F; Opacity-=0.02F);

zapiszPozycjeOkna();

}

Na wątkach spocznie obowiązek obserwowania tego pola formy i kontrola, czy nie jest ona zamykana. Dzięki temu nie będziemy musieli przechowywać referencji do obiektów obsługujących wątki w celu ich jawnego przerwania (co zresztą również nie jest proste).

Korzystanie z komponentu Timer

Rysunek 2. Na pierwszym etapie uzyskaliśmy nie magiczne gwiazdki, ale płatki śniegu.

Rysunek 2. Na pierwszym etapie uzyskaliśmy nie magiczne gwiazdki, ale płatki śniegu.

Zgodnie z paradygmatem programowania obiektowego zaprojektujemy klasę Gwiazdka, odpowiedzialną za rysowanie naszych gwiazdek i zawierającą, oczywiście, metodę rysującą gwiazdkę. Będzie to metoda prywatna, wywoływana z konstruktora klasy. Zatem, aby utworzyć nową gwiazdkę, trzeba utworzyć nowy obiekt klasy Gwiazdka. Dla użytkownika tej klasy jedno i drugie będzie równoznaczne.

class Gwiazdka

{

//pola

private const int r = 4;

public static Form1 forma = null;

public static Bitmap obraz = null;

public static Random random = null;

//statyczna metoda pomocnicza rysująca

//gwiazdkę o zadanym kolorze

private static void rysujGwiazdke(Graphics g

int x, int y, Color kolor, bool ksztalt)

{

if (forma.zamykanie) return;

if (!ksztalt)

{

//kształt koła

g.FillEllipse(new SolidBrush(kolor)

x-r,y-r,2*r,2*r);

}

else

{

//kształt gwiazdy

Pen pioro = new Pen(kolor,1);

g.DrawLine(pioro,x-r,y,x+r,y);

g.DrawLine(pioro,x,y-r,x,y+r);

g.DrawLine(pioro,x-r,y-r,x+r,y+r);

g.DrawLine(pioro,x+r,y-r,x-r,y+r);

}

}

//konstruktor

public Gwiazdka()

{

if (forma==null||obraz==null||random==null)

throw new Exception("Przed utworzeniem

obiektu należy zainicjować statyczne

pola forma, obraz i random");

rysujGwiazdke(forma.CreateGraphics()

r+random.Next(obraz.Width-2*r)

r+random.Next(obraz.Height-2*r)

Color.Cyan, true);

}

}

Rysunek 3. Warto przestudiować także przykłady w dokumentacji klas Timer, Thread i Mutex.

Rysunek 3. Warto przestudiować także przykłady w dokumentacji klas Timer, Thread i Mutex.

Klasa ma cztery pola. Pierwsze, o nazwie r, określa wielkość gwiazdki. Kolejne trzy są polami statycznymi (tj. ich wartość jest wspólna dla wszystkich instancji klasy). Przechowują referencje do obiektów istotnych z punktu widzenia rysowania w formie. Są to: referencja do samej formy, do obiektu Bitmap przechowującego obraz choinki i do generatora liczb pseudolosowych. Dlaczego chcę używać jednego wspólnego generatora we wszystkich obiektach klasy Gwiazdka, zamiast tworzyć własny generator w każdej z nich? Okazuje się, że w drugim wypadku pojawiają się niepożądane regularności w układzie gwiazdek. Znacznie lepsze rezultaty daje korzystanie z jednego wspólnego generatora. Zwróćmy także uwagę na to, że pole forma jest typu Form1, a nie standardowego Form. Dzięki temu bez dodatkowych rzutowań zawsze uzyskamy dostęp do pola zamykanie, zdefiniowanego w naszej klasie formy. Jest ono, co prawda, prywatne, ale klasa Gwiazdka to, używając nomenklatury Javy, klasa wewnętrzna, a więc ma dostęp także do prywatnych pól klasy Form1. Należy jeszcze podkreślić, że statyczne pola forma, obraz i random muszą być zainicjowane przed utworzeniem pierwszego obiektu typu Gwiazdka, bo są wykorzystywane w jego konstruktorze.

Konstruktor klasy robi w tej chwili tylko jedno - wywołuje metodę pomocniczą, która rysuje gwiazdkę. Ciekawe są jednak parametry wykorzystywane do wywołania tej metody. Pierwszy to obiekt typu Graphics, którego metody wykorzystujemy do rysowania w formie. Tworzymy go metodą CreateGraphics wywołaną dla obiektu formy przechowywanej w statycznym polu forma. Dwa kolejne argumenty to współrzędne rysowanej gwiazdki. Wytyczamy je, korzystając z generatora liczb pseudolosowych i uważając, aby żadne jej ramię nie wystawało poza obręb formy. Czwartym argumentem jest kolor gwiazdki, w naszym wypadku błękitny (Color.Cyan). I wreszcie ostatni argument to wartość logiczna określająca, czy gwiazdka ma mieć kształt koła (wartość false), czy ośmioramienny (true).


Zobacz również