Artykuł

sie 22 2012
0

Wielowątkowość w C# - wprowadzenie

Nie wiem jak Was, ale mnie osobiście irytuje zdarzenie w którym na górnej belce aplikacji pojawia się napis Brak odpowiedzi, a sam program wygląda jakby się gdzieś po drodze pogubił... Generalnie taki stan rzeczy może być spowodowany wieloma różnymi aspektami, choć zasadniczo w tym przypadku najczęściej można wskazać dwie przyczyny. Trzeba niestety tu otwarcie przyznać, że w głównej mierze są one spowodowane źle napisanym kodem przez programistów tychże aplikacji.

Po pierwsze, w każdym programie może po prostu pójść coś nie tak. Programista powinien przewidzieć możliwie większość takich sytuacji (najlepiej wszystkie, co w praktyce jest jednak raczej nierealne, jeśli tworzymy coś więcej niż program typu Silnia) i oprogramować jakieś sensowne wyjście z problemu - czy to takie które po prostu zakończy aktualnie wykonywaną czynność, czy w przypadku błędów krytycznych, spowoduje bardziej przyjazne dla użytkownika zamknięcie aplikacji (przez bardziej przyjazne mam na myśli jakiś komunikat dla użytkownika i odpowiedni zapis w logu).

Drugą kwestią jest właśnie tytułowa wielowątkowość. Jeśli tworzymy program, którego operacje przetwarzania mogą zająć przynajmniej kilka sekund, to użycie wątków staje się koniecznością (szczególnie jeśli tworzymy aplikację z GUI, o czym pisałem już choćby przy okazji opisu Background Workera dla WPF). Wielowątkowość pozwala również na lepsze wykorzystanie wielordzeniowych procesorów, ponieważ dzięki temu operacje mogą rozłożyć się na kilka rdzeni, a nie tylko na jeden, na którym początkowo została uruchomiona dana aplikacja. Ponieważ nie jest to zagadnienie proste, to chciałbym poświęcić mu ten oraz kilka kolejnych wpisów. Zacznijmy więc od podstaw.

Istota wielowątkowości

Istota wielowątkowości zasadniczo jest prosta - chodzi przede wszystkim o maksymalne wykorzystanie dostępnych zasobów i zrównoleglenie kilku wykonywanych naraz akcji. Każda domyślnie stworzona aplikacja posiada tylko jeden wątek główny, co widoczne jest szczególnie w przypadku aplikacji z GUI. Jeśli w tym przypadku wątek główny zacznie wykonywać jakąś złożoną operację, nasz program nie będzie w stanie wykonać cyklicznej operacji odświeżania interfejsu i mimo, że formalnie program będzie wciąż działać, to aplikacja znajdzie się w klasycznym stanie braku odpowiedzi.

Czym zatem formalnie jest wątek? Niczym innym jak swego rodzaju jednostką/agentem odpowiedzialnym za wykonywanie określonego kodu. Naszymi wątkami zarządza system. Framework .Net tylko opakowuje systemowe wątki za pomocą klasy Thread. Tak jak wspomniałem wcześniej, każdy program posiada przynajmniej jeden wątek. O jego powstanie nie musimy się martwić, dzieje się to nie jako automatycznie. Nie znaczy to oczywiście, że w trakcie działania aplikacji nie możemy się odwołać do wątku głównego - jest to jak najbardziej możliwe:)

Priorytety wątków

Każdy z utworzonych przez nas wątków, posiada określony priorytet działania, wyrażony jako wartość enumeracji ThreadPriority. My możemy wybrać jedną z pięciu wartości:

  • Lowest
  • BelowNormal
  • Normal
  • AboveNormal
  • Highest

Każda kolejna wartość reprezentuje rosnący priorytet, a domyślną wartością jest oczywiście Normal. Dziś priorytety wątków, nie mają już tak dużego znaczenia, jak miało to miejsce kiedyś (szczególnie jeśli tworzymy proste, kilku-wątkowe aplikacje). Jest to spowodowane przede wszystkim dużą mocą obecnych komputerów oraz liczbą rdzeni znajdujących się w obecnych procesorach.

Manipulacja priorytetami wątków, może mieć również swoje negatywne konsekwencje. Nawet wątki o najwyższym priorytecie mogą być zablokowane przez inne wątki. Wątki o wysokim priorytecie mogą ponadto konkurować z wątkami systemowymi co może mieć negatywny wpływ na ogólną wydajność systemu operacyjnego.

Warto również nadmienić, że sam system monitoruje wszystkie wątki i jeśli tylko zauważy jakiś, który został dawno nieuruchomiony, sam podwyższy tymczasowo jego priorytet aby umożliwić mu pracę.

Reasumując - priorytety wątków to istotne zagadnienie, jednak jeśli wszystko w Waszej aplikacji działa dobrze, to lepiej ich nie ruszać - chyba , że macie pełną świadomość tego co robicie;)

Stan wątku

Każdy utworzony wątek posiada swój stan, który później może zmieniać się wielokrotnie w czasie jego pracy. My jako programiści możemy oczywiście na ten stan wpływać, choćby poprzez uruchomienie, wstrzymanie, czy też zatrzymanie wątku. Skoro stan wątku przyjmuje pewne określone wartości, wydaje się być logicznym żeby opisać je za pomocą enumeracji. Tak też pomyśleli również programiści Microsoftu, którzy przygotowali enumerację ThreadState z 10 różnymi wartościami:

  • Running = 0
  • StopRequested = 1
  • SuspendRequested = 2
  • Background = 4
  • Unstarted = 8
  • Stopped = 16
  • WaitSleepJoin = 32
  • Suspended = 64
  • AbortRequested = 128
  • Aborted = 256

Tak jak wspomniałem wcześniej, większość tych stanów związana jest bezpośrednio z naszymi działaniami na wątku, część z nich jest następstwem innych statusów. Tylko właściwie stan Stopped wynika z naturalnego zakończenia pracy wątku.

Aby lepiej zrozumieć stany wątku oraz metody z nimi powiązane, spójrzcie na poniższy rysunek:

Na każdej ze strzałek widoczna jest wywołująca określony stan metoda. Wewnątrz poszczególnych kształtów znajduje się ich konkretne wartości enumeracji.

Skoro mamy już pewne wstępne teoretyczne informacje na temat wątków, możemy w końcu przyjrzeć się, jak to wszystko wygląda w praktyce.

Wątek główny aplikacji

Tak jak wspomniałem wcześniej, każda z aplikacji napisanych w C# posiada swój wątek główny, do którego dostęp możemy uzyskać poprzez zastosowanie konstrukcji:

Thread.CurrentThread

Aby taka konstrukcja jednak zadziałała, konieczne jest podpięcie odpowiedniej przestrzeni nazw:

using System.Threading;

Na wątku głównym możemy wywołać wiele różnych operacji, bądź też zmienić lub pobrać wartości określonych właściwości. Wcześniej mówiłem o priorytetach wątków. Te możemy zmienić, bądź sprawdzić za pomocą właściwości Priority. Wspominałem również o enumeracji opisującej aktualny stan wątku. Jej aktualną wartość znajdziemy we właściwości ThreadState.

Każdy wątek posiada również właściwości związane z ustawieniami regionalnymi, o których wspominałem choćby przy wpisie na temat tworzenia aplikacji wielojęzycznych. Wątek główny determinuje te ustawienia dla całej aplikacji.

Po więcej metod oraz właściwości odsyłam do dokumentacji, do której link znajdziecie również u dołu strony. Część z metod oraz właściwości zostanie również zaprezentowana w dalszej części wpisu.

Nasz pierwszy wątek!

Być może wątek główny aplikacji jest interesujący, ale w tym momencie można powiedzieć, że jest bardzo samotny i potrzebuje nowego towarzysza. Aby ukrócić jego cierpienia, napiszemy pewien kawałek kodu:

using System.Diagnostics;
using System.Threading;

namespace Threads
{
    class Program
    {
        static void Main(string[] args)
        {
            MyThreadClass oMyThreadClass = new MyThreadClass();
            Thread oThread = new Thread(new ThreadStart(oMyThreadClass.Run));
            oThread.Start();
            Debug.WriteLine("Oczekiwanie na zakończenie wątku...");
            oThread.Join();
        }
    }

    class MyThreadClass
    {
        public MyThreadClass()
        {
        }

        public void Run()
        {
            Debug.WriteLine("Rozpoczynam pracę...");
            // Jakieś długie operacje
            Debug.WriteLine("Test uśpienia wątku...");
            Thread.Sleep(500);
            Debug.WriteLine("Kończę pracę...");
        }
    }
}

Dzieje się tutaj kilka ciekawych rzeczy. Po pierwsze tworzymy klasę MyThreadClass a w niej metodę Run, która będzie wykonywać pewne długie operacje (27). Wewnątrz metody zapisujemy sobie również informacje dodatkowe do okienka (26,28,30), dzięki czemu łatwiej będzie prześledzić sam proces. Po wykonaniu naszych długich operacji, testujemy operację uśpienia (29). Wartość 500 określa liczbę milisekund. Przez ten czas, wątek będzie uśpiony.

W klasie głównej, tworzymy obiekt naszej klasy MyThreadClass (10), a następnie tworzymy obiekt wątku, w którym wskazujemy na metodę Run jako tą, która ma być wykonywana w ramach wątku (11). Następnie uruchamiamy wątek (12).

Ostatnim krokiem będzie wywołanie metody Join na naszym wątku, która oczekuje na jego zakończenie. Bez tej metody, istniałoby spore prawdopodobieństwo, że wątek główny zakończy się zanim wykonane zostaną wszystkie operacje przewidziane dla wątku pobocznego. A tego byśmy nie chcieli...

Przekazywanie parametrów do metody wątkowej

Pewnym niedociągnięciem powyższego rozwiązania może być brak możliwości przekazania parametrów do świeżo utworzonego wątku. Na szczęście ten problem można bardzo szybko obejść, za sprawą wątków sparametryzowanych, które przyjmują obiekt danych. Obiektem może być pojedynczy string, liczba czy choćby nasza własna klasa.

Załóżmy, że chcielibyśmy na podstawie danych wprowadzonych od użytkownika, obliczyć wartość jego BMI. Zasadniczo moglibyśmy po kolei spytać o wzrost i wagę, lub poprosić użytkownika o przekazanie danych w odpowiednim formacie. W swoim rozwiązaniu oczekuję od użytkownika danych w postaci wzrost;waga, przy czym wzrost wyrażony jest w metrach np. 1,75; a waga w kg np. 70. W ten sposób otrzymuję parę w postaci:

1,75;70

Warto tutaj zwrócić na ustawienia regionalne w systemie - kropka czy przecinek (dla polskich ustawień regionalnych będzie to oczywiście przecinek).

Spójrzmy na poniższy listing, celem szerszej analizy:

using System.Diagnostics;
using System.Threading;
using System;

namespace Threads
{
    class Program
    {
        static void Main(string[] args)
        {
            MyThreadClass oMyThreadClass = new MyThreadClass();
            Console.WriteLine(
				"Wprowadź wzrost w m oraz wagę w kg w postaci \"wzrost;waga\"");
            string sData = Console.ReadLine();
            Thread oThread = new Thread(new ParameterizedThreadStart(
				oMyThreadClass.GetBmi));
            oThread.Start(sData);
            oThread.Join();
        }
    }

    class MyThreadClass
    {
        public MyThreadClass()
        {
        }

        public void GetBmi(object oData)
        {
            string[] asData = oData.ToString().Split(';');
            Console.WriteLine(string.Format("Twoja wartość BMI: {0:f}",
                int.Parse(asData[1]) / Math.Pow(double.Parse(asData[0]), 2)));
            Console.WriteLine("Naciśnij dowolny klawisz...");
            Console.ReadKey();
            
        }
    }
}

Zacznijmy od klasy, która zawiera metodę GetBmi, z której ma skorzystać nasz docelowy wątek. Przyjmuje ona jeden parametr będący obiektem o czym wspominałem wcześniej. W moim przypadku jest to string, który muszę podzielić po średniku (30). Następnie wypisuję użytkownikowi Jego BMI (31-32) i oczekuje na naciśnięcie klawisza (34), który zakończy pracę wątku.

W klasie głównej, musimy pozyskać od użytkownika dane (14) oraz utworzyć sparametryzowany wątek (15-16). Resztę wykonywanych tutaj operacji już znacie, więc pominę ich opis.

Oczywiście w normalnej aplikacji szkoda byłoby zachodu by tworzyć wątek tylko do obliczania BMI. W tym przypadku jest to jednak tylko aplikacja pokazowa i chodziło o coś prostego celem demonstracji;)

Jeśli sparametryzowane wątki to dla Was za mało, to w jednym z kolejnych wpisów postaram się Was zapoznać z zagadnieniem współbieżności wątków, dzięki czemu wymiana danych możliwa jest w czasie rzeczywistym.

Przerywanie wątku

Z reguły naszym celem będzie wykonanie wszystkich operacji przeznaczonych do realizacji w określonym wątku, czasem jednak w sytuacjach krytycznych może zaistnieć konieczność przerwania wątku. Aby zrealizować to zadanie, będziemy musieli wykonać metodę Abort, która przerywa działanie wskazanego wątku (używanie tej metody jest niezalecane, ponieważ wywołuje ona wyjątek ThreadAbortException, przed którym należy się zabezpieczyć). Wcześniej warto jednak się upewnić, czy aby na pewno wskazany wątek jest aktywny (żywy). Spójrzmy jeszcze raz, na delikatnie zmodyfikowaną klasę główną:

using System.Diagnostics;
using System.Threading;

namespace Threads
{
    class Program
    {
        static void Main(string[] args)
        {
            MyThreadClass oMyThreadClass = new MyThreadClass();
            Thread oThread = new Thread(new ThreadStart(oMyThreadClass.Run));
            oThread.Start();
            if (oThread.IsAlive)
            {
                oThread.Abort();
            }
        }
    }
}

Podsumowanie

To by było na tyle ile chciałbym Wam powiedzieć w pierwszym wpisie na temat wątków. W kolejnym wpisie postaram się napisać przede wszystkim o synchronizacji wątków, dzięki czemu współdzielenie danych między wątkami stanie się znacznie łatwiejsze. Nie ukrywam, że przyszłość dalszych tego wpisów uzależniona jest od Waszego zainteresowania tematem, zobaczymy więc jak to wszystko się potoczy;)

Literatura

Data ostatniej modyfikacji: 06.09.2012, 21:42.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus