Klasa Thread

underTaker

Wątek

Jak opisałem w poprzednim artykule - wątek jest używany do wykonywania wielu operacji w tym samym czasie bez blokowania interfejsu programu, czy wykonywania innych operacji. Jako przykład podana została najprostsza forma użycia wątku. Co jednak, jeśli chcemy do naszego wątku przekazać jakiś parametr? Otóż klasa Thread posiada kilka konstruktorów, z czego najczęściej używany to:

  • Thread(ThreadStart) - Najprostsza forma wątku, przyjmująca jako parametr delegat ThreadStart. Nie przyjmuje on żadnych parametrów oraz nie zwraca żadnych wartości - prosty "pojemnik" na metodę.

  • Thread(ParameterizedThreadStart) - Tego konstruktora używamy, gdy chcemy do naszego wątku przekazać jakieś parametry.

Klasa Thread posiada również 2 dodatkowe konstruktory wyglądająca identycznie jak opisane powyżej, z dodatkową zmienną int określającą maksymalny rozmiar stosu wywołań.

ParameterizedThreadStart

O ile w poprzednim artykule poznaliśmy podstawy klasy Thread używając konstruktura z ThreadStart, to teraz poznamy użycie konstruktora ParameterizedThreadStart, pozwalającego przekazać parametry do naszego wątku.
Załóżmy, że chcemy rozbudować przykład z poprzedniego artykułu o możliwość przekazania jako parametr ilości iteracji, które zostaną wykonane. Nie można tego zrobić za pomocą delegatu ThreadStart, gdyż nie przyjmuje on żadnych parametrów. Tutaj z pomocą przychodzi wspomniany delegat ParameterizedThreadStart, przyjmujący jako parametr zmienną typu object. Co za tym idzie, musimy zmienić nagłówek naszej metody w taki sposób:

static void doSomething(object pm)
...

Parametrem musi być object, a co za tym idzie następnie musi zostać wykonany tzw. unboxing, czyli rzutowanie na inny typ. W naszym wypadku chcemy przekazać do wątku ilość iteracji, która jest typem liczbowym - więc rzutujemy na inta:

int iter = (int)pm;

A jako użyte w poprzednim przykładzie 10000 iteracji podajemy właśnie zmienną iter. To wszystko, co chcemy zmienić w wątku. Co jednak z samą zmienną Thread? Tutaj sytuacja wygląda równie prosto. Nie musimy zmieniać deklaracji wątku, bo kompilator sam "dopasuje" naszą metodę do odpowiedniego delegatu. Pozostaje jedynie przekazanie parametrów, co robimy w funkcji Start, która posiada jedno przeładowanie - przyjmujące, jak się zapewne domyślacie, parametr typu object - jemu przekazujemy własnie ilość iteracji, które chcemy wykonać.

Wątki a Windows Forms

Sytuacja nieco się komplikuje, gdy do tego wszystkiego wmieszamy aplikacje okienkowe. Załóżmy, że chcemy wykonać czasochłonną operację w tle, i powiadomić użytkownika o jej zakończeniu przez wyświetlenie komunikatu na kontrolce RichTextBox. Logicznie rzecz biorąc sytuacja będzie podobna do tego, co robiliśmy do tej pory. I w istocie tak jest. Napotkamy jednak problem, gdy chcemy zmienić zawartość kontrolki. Wyrzucony zostanie wyjątek, o podobnej treści:

Cross-thread operation not valid: Control 'richTextBox1' accessed from a thread other than the thread it was created on.

Pytanie - co on oznacza? Nie można wykonać operacji międzywątkowej. Dlaczego tak się dzieje? Kontrolka nie może być użyta przez kilka wątków jednocześnie. Ściślej - może być użyta tylko i wyłącznie przez wątek, w którym została stworzona. To jednak wcale nie oznacza, że nie możemy z tym nic zrobić. Kontrolki oraz formy zawierają metodę Invoke, oraz właściwość InvokeRequired.

Metoda invoke służy do wywołania kontrolki z wątku, w którym została ona utworzona, a właściwość InvokeRequired informuje nas, czy taka operacja jest konieczna. Oczywiście nic złego się nie stanie, jeśli użyjemy Invoke, gdy to nie jest konieczne.

Zakładając, że nasz kod wygląda następująco:

void doSomething()
{
    for (int i = 0; i <= 10000; i++) ;
    richTextBox1.AppendText("Wykonano wszystkie iteracje!");
}

...

public Form1()
{
   InitializeComponent();

    richTextBox1.AppendText("Działanie w toku...");
    Thread thr = new Thread(doSomething);
    thr.Start();
}

Musimy rozbudować naszą metodę doSomething o użycie właśnie funkcji Invoke. Dopisujemy do tego celu nową metodę, dajmy na to @@AppendText(string str)@@, w której sprawdzimy, czy konieczne jest odwołanie się do naszego richTextBox1 przez invoke, czy też nie. Wyglądać będzie tak:

void appendText(string str)
{
    if (richTextBox1.InvokeRequired)
        richTextBox1.Invoke(new Action<string>(appendText), str);
    else
        richTextBox1.AppendText(str);
}

Najpierw sprawdzamy, czy konieczne jest użycie invoke, jeśli tak, to wykonujemy invoke w richTextBox1. Metoda ta pobiera jako parametr Delegate oraz listę parametrów metody, którą przekazujemy jako delegat. W tym wypadku użyłem gotowego delegatu generycznego Action (typy generyczne opisane zostały w artykule Typy generyczne), oraz jako parametr przekazałem parametr str, z naszej metody appendText. Użycie tak skonstruowanego Invoke powoduje wywołanie naszej metody appendText w podanym parametrem, jednak z tą różnicą, że InvokeRequired będzie równe False, przez co wykonana zostanie ostania linijka dodająca tekst - tym razem jednak bez żadnego wyjątku, bo richTextBox1 został właśnie wywołany za pomocą głównego wątku programu.
Natomiast w naszej metodzie doSomething zmieniamy

richTextBox1.AppendText("Wykonano wszystkie iteracje!");

na

appendText("Wykonano wszystkie iteracje!");
Invoke - wersja krótsza

Pokazany powyżej zapis jest prawdę mówiąc męczący - tworzenie nowej metody za każdym razem, gdy chcemy cokolwiek zmienić w kontrolce z innego wątku. Jest sposób i na to, co prawda nie jest to ściśle związane z wielowątkowością, ale myślę, że warto o tym chociaż tutaj wspomnieć. Użyjemy do tego celu metod anonimowych oraz Linq.

Zamiast tworzyć nową metodę, wywołamy od razu Invoke w taki sposób:

richTextBox1.Invoke(new Action(delegate()
    {
        richTextBox1.AppendText("Wykonano wszystkie iteracje!");
    }));

Na pierwszy rzut oka taki kod nie jest zbyt schludny, dla mnie jednak taki zapis jest o wiele bardziej korzystny - nie musimy tworzyć nowych metod specjalnie dla jednej operacji, której nie użyjemy do niczego innego - wybór pozostawiam wam.

1 komentarz

Można by coś niecoś dopisać jeszcze, ale i tak wytłumaczone najlepiej w internecie. NAprawde 100% zadowolenia.