Skip to main content

Welowątkowość – podstawy

Wielowątkowość (ang. multithreading) – cecha systemu operacyjnego, dzięki której w ramach jednego procesu może wykonywać kilka wątków lub jednostek wykonawczych. Nowe wątki to kolejne ciągi instrukcji wykonywane oddzielnie. Wszystkie wątki tego samego procesu współdzielą kod programu i dane.

Źródło: wikipedia.org

Wątek (ang. thread) – część programu wykonywana współbieżnie w obrębie jednego procesu; w jednym procesie może istnieć wiele wątków.

Różnica między zwykłym procesem a wątkiem polega na współdzieleniu przez wszystkie wątki działające w danym procesie przestrzeni adresowej oraz wszystkich innych struktur systemowych (np. listy otwartych plików, gniazd itp.) – z kolei procesy posiadają niezależne zasoby.

Źródło: wikipedia.org

Dzisiejszym tematem chcę zapoczątkować serię artykułów na temat wątków.

Jest to jeden z trudniejszych tematów z jakim zetknąć się można w programowaniu, który zdecydowałem się podzielić na kilka mniejszych, w których zostaną omówione kolejno:

  1. Wątki.
    • Worker threads,
    • Event-driven threads.
  2. Synchronizacja.
    • Semafory,
    • Muteksy,
    • Monitory.

Trudny z tego powodu, że nieumiejętne korzystanie z dobrodziejstw jakie niesie wielowątkowość, może spowodować nieoczekiwane efekty jak: problemy z synchronizacją wątków przy dostępie do współdzielonych zasobów (np. zmienne), zakleszczenie (ang. Deadlock) wątków, prowadzące w konsekwencji do całkowitego zawieszenia się aplikacji.

Na domiar tego, rozróżniamy dwa rodzaje wątków.

Worker threads – po wykonaniu zadania w ciele metody Run(), wątek zostaje automatycznie zakończony.

Event-driven threads – wątek z obsługą zdarzeń/powiadomień, działający do chwili uzyskania sygnału zatrzymania.

Poniższej obrazowo przedstawione zostały różnice w czasie życia wątków.

Jak zwykle w trakcie opisywania pewnych zagadnień, najtrudniejszym jest ukazanie tego na praktycznym przykładzie.

Zadaniem wymagającym pracy w tle może być np. wykonywanie dużej liczby obliczeń matematycznych, które wymagają wiele czasu na wykonanie, a jednocześnie chcielibyśmy mieć pełną kontrolę nad interfejsem użytkownika (brak efektu zamrożenia), przełączać się między formularzami i zakończenia tego zadania, jeśli stwierdzimy, że trwa ono zbyt długo.

Tradycyjne zadanie

Pierwsze zdanie będzie polegało na wyświetleniu dziesięciu komunikatów w odstępie co jedną sekundę na konsolę Output.
Na razie jest to zwykła funkcja nie oparta o wątki, która ma Ci ukazać, jaki będzie efekt jej wywołania w Twojej aplikacji.
A efektem tym będzie niemożność kontroli aplikacji przez interfejs użytkownika, dopóki zadanie nie zostanie zakończone.

void DoWork()
{
  int counter = 1;
  while(counter <= 10)
  {
    AppLogDebug("Counter: %d", counter);

    // czekaj 1 sekundę
    Osp::Base::Runtime::Thread::Sleep(1000);

    counter++;
  }
}

Zadanie w oddzielnym wątku typu Worker

Należy stworzyć nową klasę zadania (MyTask) dziedziczącą po klasie Object i zaimplementować interfejs IRunnable, który wymaga zaimplementowania tylko jednej metody:

Osp::Base::Object* Run(void);

class MyTask :
  public Osp::Base::Object,
  public Osp::Base::Runtime::IRunnable
{
public:
  Object* Run(void)
  {
    int counter = 1;
    while(counter <= 10)
    {
      AppLogDebug("Counter: %d", counter);
      Osp::Base::Runtime::Thread::Sleep(1000);
      counter++;
    }

    return null;
  }
}

Następnie tworzymy nowy wątek, który jako parametr przyjmie nasze zadanie MyTask do wykonania .

MyTask* pTask = new MyTask();
Thread* pThread = new Thread();

pThread->Construct(*pTask);
pThread->Start();

delete pTask;

Tym razem osiągnęliśmy efekt zamierzony. Nasze zadanie wykonuje się w tle bez efektu zamrażania interfejsu użytkownika.
Jeśli zdecydowalibyśmy się wyprowadzić klasę wątku bezpośrednio po klasie Thread (zamiast implementować tylko interfejs IRunnable), mielibyśmy dodatkowo szereg metod pozwalających określić stan naszego wątku za pomocą zdarzeń:

  • OnStart();
  • OnStop();

Wątek typu Worker będzie działał dopóty, dopóki nie nastąpi wyjście z metody Run().

Kończenie zadania w wątku Worker

Nasze przykładowe zadanie jest zaprogramowane na 10 sekund pracy. Po tym czasie zostanie automatycznie zakończone.
Nie wszystkie zadania są tak proste i nie zawsze warunek umożliwiający poprawne zakończenie zadania jest spełniony (np. błąd programisty).

Kolejnym powodem chęci wcześniejszego zakończenia zadania jest decyzja użytkownika, który chce zakończyć zadanie lub zamknąć aplikację.
Nie możemy kazać mu czekać pełnych 10 sekund, aż nasz program łaskawie zezwoli na zamknięcie i zwolni wszystkie zajęte przez siebie zasoby 🙂

Poniżej zmodyfikowana klasa zadania MyTask() posiadająca dodatkowy warunek sprawdzający wartość zmiennej statycznej typu logicznego, która w tym przykładzie jest zdefiniowana w formularzu.

class MyTask :
  public Osp::Base::Object,
  public Osp::Base::Runtime::IRunnable
{
public:
  Object* MyTask::Run(void)
  {
    int counter = 1;
    while(counter <= 10)
    {
        if (Form1::keepAlive == false) break;
        AppLogDebug("Counter: %d", counter);
        Thread::Sleep(1000);
        counter++;
    }

    // zwolnij zajęte zasoby

    // Jako wynik tej metody zawsze zwracamy wartość null, ponieważ zwracany obiekt nie jest nigdzie i nigdy wykorzystywany przez platformę bada.
    return null;
  }
}

Uruchomienie zadania prawie się nie zmienia:

MyTask* pTask = new MyTask();
Thread* pThread = new Thread();

keepAlive = true;

pThread->Construct(pTask);
pThread->Start();

delete pTask;

Aby zakończyć zadanie, nie musimy już czekać, aż zostanie wykonane do końca.
Wystarczy, że ustawimy zmienną keepAlive na false, a zadanie zostanie zakończone w możliwie najszybszym czasie, zależnie od naszego zadania.

if (keepAlive)
{
  AppLog("Kończę wątek...");
  keepAlive = false;

  // czekaj na zakończenie wątku
  pThread->Join();

  delete pThread;
  pThread = null;

  AppLog("Wątek zakończony.");

  // można zamknąć aplikację
}

Wynik działania

Zrzut ekranu przedstawia komunikaty generowane przez aplikację po uruchomieniu zadania przyciskiem Start Task in Thread.
Kiedy licznik wskazał liczbę 5, przerwałem działanie wątku przyciskiem Stop Thread.

Program do pobrania: Worker.zip (starsza wersja).

W kolejnej części opiszę wątek typu Event-driven.

markac

Full-stack Web Developer

4 thoughts to “Welowątkowość – podstawy”

  1. Witam,
    do artykułu wdarł się błąd.
    W części, gdzie mamy opisane rodzaje wątków, ich definicje zostały zamienione, co wynika z rysunku zamieszczonego niżej.

    Również przykład opisuje działanie wątku typu „Event-driven”, a nie typu „Worker”.

    Pozdrawiam

  2. Definicje zostały poprawione.

    Przykład jest Workerem, ponieważ implementuje metodę Run() interfejsu IRunnable. Dla wątków typu Event-driven nie można przesłaniać tej metody, którą dostarcza klasa Thread, gdyż wątek zacznie się zachowywać właśnie jak Worker. Bez znaczenia jest to, czy w konstruktorze podamy THREAD_TYPE_WORKER czy THREAD_TYPE_EVENT_DRIVEN. Sprawdzone doświadczalnie…

Komentarze są zamknięte.