C Helpdesk

C Helpdesk

  • Nie jesteś zalogowany.
  • Polecamy: Moda

  • Index
  •  » Tutorial
  •  » [C/C++]Wątki - kontekst, szeregowanie i priorytety

#1 04-01-2007 00:49:42

mieczyk

Member

3375619
Zarejestrowany: 03-08-2007
Posty: 24
Punktów :   
WWW

[C/C++]Wątki - kontekst, szeregowanie i priorytety

---------------------------------------------------------------

0x01. Wstęp.
0x02. Inicjalizacja nowego wątku.
0x03. Kontekst wątku.
0x04. Usypianie i przełączanie na inny wątek.
0x05. Priorytety - teoria.
0x06. Priorytety - praktyka.
0x07. Dynamiczne podwyższanie priorytetów.
0x08. Bibliografia.

---------------------------------------------------------------


---------------------------------------------------------------
0x01. Wstęp
---------------------------------------------------------------

Ten artykuł stanowi kontynuację tekstu  "Wątki - wprowadzenie". Do zrozumienia niniejszego tekstu wymagana jest znajomość WinAPI i C++. W przykładach posługiwałem się darmowym środowiskiem programistycznym Dev-C++ 4.9.9.2. Część występujących tu zagadnień została omówiona w moich wcześniejszych artykułach: Wątki - wprowadzenie i Współużytkowanie obiektów jądra w systemie Windows.

W razie błędów merytorycznych lub technicznych bardzo proszę o sprostowanie.

---------------------------------------------------------------
0x02. Inicjalizacja nowego wątku
---------------------------------------------------------------

W moim wcześniejszym artykule zatytułowanym "Wątki - wprowadzenie" pokazałem w jaki sposób można utworzyć nowy wątek w przestrzeni procesu. Teraz chcę w skrócie przedstawić co dzieje się w systemie podczas tworzenia i inicjalizacji nowego wątku.

Kiedy wywołujemy funkcję CreateThread z odpowiednimi parametrami, tworzony jest w systemie nowy obiekt-wątek i inicjalizowane są pewne jego właściwości takie jak: licznik użyć, licznik zawieszeń, kod wyjścia i stan obiektu.

Jak wcześniej wspominałem, możliwe jest zawieszenie nowego wątku za pomocą odpowiedniej flagi (lub funkcji, ale o tym później). Kiedy tworzymy nowy wątek, to wstępnie licznik zawieszeń ustawiany jest na 1 (tak się dzieje zawsze). Jeśli użyliśmy flagi CREATE_SUSPENDED (patrz: Wątki - wprowadzenie ) to licznik ten jest inkrementowany, a sam wątek nie wykona się dopóty, dopóki licznik zawieszeń nie dojdzie do 0.

Przy inicjalizacji wątku (na wstępie), kod wyjścia ustawiany jest na wartość STILL_ACTIVE (0x103), stan obiektu na niesygnalizowany, licznik użyć obiektu zawiera wartość 2 i dla przypomnienia licznik zawieszeń wynosi 1 (bez flagi CREATE_SUSPENDED).

Nasz obiekt wątek jest już utworzony. Teraz system alokuje pamięć przeznaczoną na stos wątku, a pochodzi ona z przestrzeni adresowej procesu, do którego tworzony wątek należy.

Następnie system zapisuje dwie wartości w najwyższym adresie stosu nowego wątku. Pierwszą z nich jest parametr pvParam funkcji CreateThread (ten parametr określa wartość przekazywaną do funkcji wątkowej), a drugą pfnStartAddr (też jest parametrem funkcji CreateThread i wskazuję nazwę, a właściwie adres funkcji wątkowej).

Każdy wątek, oprócz stosu ma "swój własny" zbiór rejestrów CPU zwany kontekstem wątku. Kontekst przedstawia stan rejestrów CPU podczas ostatniego wykonywania wątku. Zbiór rejestrów CPU wątku jest przechowywany w strukturze CONTEXT, która z kolei jest zawarta w obiekcie-wątku.

Rejestr wskaźnika instrukcji (IP) i rejestr wskaźnika stosu (SP) są najważniejszymi rejestrami w kontekście wątku. Podczas inicjalizacji obiektu-wątku rejestr SP (w strukturze CONTEXT) jest ustawiany na adres, pod którym został umieszczony na stosie parametr pfnStartAddr. Rejestr IP natomiast, wskazuje na adres funkcji BaseThreadStart (która jest nieudokumentowana i nie eksportowana).

Po zakończeniu inicjalizacji wątku system sprawdza, czy podczas tworzenia wątku została użyta flaga CREATE_SUSPENDED. Jeżeli nie, licznik zawieszeń jest zmniejszany do 0, a wątek jest ustawiany w kolejce do wykonania. Gdy nadejdzie kolejka tego wątku, system  ładuje do właściwych rejestrów CPU wartości zapisane ostatnim razem w kontekście wątku i uruchamia jego kod. Wykonanie wątku rozpoczyna się od funkcji BaseThreadStart.

Powyżej zostały przedstawione czynności wykonywane przez system operacyjny podczas tworzenia nowego wątku. Dla podsumowania przedstawię to wszystko w skrótowym grafie:

----------------------------------------------------------------  ------------
| Wywołujemy funkcję CreateThread.                                         
| -   licznik użyć = 2.                                                               
| -   licznik zawieszeń = 1 (bez flagi CREATE_SUSPENDED).           
| -   kod wyjścia  = STILL_ACTIVE (0x103).                                 
| -   stan obiektu = niesygnalizowany.                                         
----------------------------------------------------------------  ------------
                            |
                            |
----------------------------------------------------------------  ------------
| Alokacja pamięci przeznaczonej na stos wątku.                         
----------------------------------------------------------------  ------------
                           |
                           |
----------------------------------------------------------------  ------------
| - SP wskazuje na adres parametru pfnStartAddr.                               
| - IP wskazuje na adres funkcji BaseThreadStart.                               
----------------------------------------------------------------  ------------
                           |
                           |
----------------------------------------------------------------  -------------------
| System sprawdza, czy została użyta flaga CREATE_SUSPENDED           
----------------------------------------------------------------  --------- ---------
           | NIE |                                                        | TAK |
           --------                                                       ---------
               /                                                                   \
              /                                                                     \
             /                                                                       \
- Ustawienie odpowiednich                                                 - Wątek jest zawieszony do
  wartości w rejestrach CPU.                                                 czasu wywołania funkcji
- Uruchomianie kodu nowego                                                ResumeThread.
  wątku.



---------------------------------------------------------------
0x03. Kontekst wątku
---------------------------------------------------------------

Jak zostało wspomniane wcześniej, kontekst wątku jest to zbiór rejestrów CPU, z których wątek korzysta. Kiedy mija czas wykonywania wątku, system Windows przerywa jego wykonanie i szuka kolejnego wątku, który nadaje się do wykonania. Aby wątek mógł później kontunuować swoje działanie od momentu zakończenia, rejestry CPU muszą mieć taką samą wartość jak w momencie zakończenia działania. Do tego właśnie służy kontekst wątku - zachowywane są tu wartości rejestrów CPU. Dzięki temu z rejestrów może korzystać teraz inny wątek, a gdy znowu przyjdzie kolej na przerwany wątek, do rejestrów CPU przekazywane są zachowane wcześniej wartości i wątek może kontunować swoje działanie. Taka czynność nazywana jest przełączaniem kontekstów.

Struktura CONTEXT pozwala więc systemowi pamiętać stan wątku i podjąć jego wykonywanie w momencie, gdy znów uzyska on dostęp do CPU. To właśnie w tej strukturze przechowywane są poszczególne wartości zawarte w rejestrach CPU.

Istotną rzeczą jest fakt, że składowe struktury CONTEXT zależą od architektury procesora. Na przykład dla x86 są to składowe  Eax, Ebx, Ecx, Edx i tak dalej, ale już dla procesora Alpha, składowe to IntV0, IntT0, IntT1, IntS0 i tak dalej. W swoim artykule skupię się na procesorach x86, ponieważ tylko taki posiadam. Dlatego w dalszej części artykułu odnoszę się do procesorów x86.

Pomimo, że nie ma jednolicie określonych składowych struktury CONTEXT to wiadomo, że składa się ona z kilku sekcji:
•      CONTEXT_CONTROL - obejmuje rejestry kontrolne CPU, takie jak wskaźnik instrukcji,           wskaźnik stosu, flagi i adres powrotny funkcji.
•      CONTEXT_INTEGER - obejmuje rejestry całkowite CPU, czyli np. Eax, Ebx, Ecx, Edi, itp.
•      CONEXT_FLOATING_POINT - obejmuje rejestry zmiennoprzecinkowe.
•      CONTEXT_SEGMENTS - obejmuje rejestry segmentowe (tylko x86).
•      CONTEXT_DEBUG_REGISTERS - obejmuje rejestry debugowania (tylko x86).
•      CONTEXT_EXTENDED_REGISTERS - rejestry rozszerzone (tylko x86).

Jedyną, stałą składową struktuty CONTEXT jest DWORD ContextFlags. Sekcje, które podałem powyżej są tak naprawdę nazwami flag. Załóżmy, że chcemy odczytać wartości poszczególnych rejestrów CPU zapisane w kontekście. Służy do tego odpowiednia funkcja, ale o tym za chwilę. Załóżmy, że chcemy odczytać wartości rejestrów Eax, Ebx, Ecx, Edx, Edi, Esi. Zauważmy, że znajdują się one w sekcji CONTEXT_INTEGER, ponieważ są to rejstry całkowite. Oznacza to, iż aby odczytać wartości składowych (które przechowują wartości poszczególnych rejestrów), musimy przed wywołaniem odpowiedniej funkcji przekazać do składowej ContextFlags wartość CONTEXT_INTEGER. Dzięki temu, będziemy mogli odczytać zrzucone wartości rejestrów całkowitych.

Oto jak może wyglądać prototyp struktury CONTEXT dla CPU x86:


typedef struct _CONTEXT
{

DWORD ContextFlags;

// Poszczególne sekcje (nazwy odpowiednich flag):
//
// CONTEXT_DEBUG_REGISTERS

DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;

// CONTEXT_FLOATING_POINT

FLOATING_SAVE_AREA FloatSave;

// CONTEXT_SEGMENTS

DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;

// CONTEXT_INTEGER

DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;

// CONTEXT_CONTROL

DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;

// CONTEXT_EXTENDED_REGISTERS

BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;



A teraz przedstawię w jaki sposób można pobrać informację o kontekście wątku:

contextA.cpp

Kod:

#include <iostream>
#include <windows.h>
#include <conio.h>

using namespace std;

DWORD WINAPI watek(PVOID pvParam)
{
  cout << "To jest nowy watek\n\n";
  return 0;
}

int main()
{
  DWORD ThreadID;
  HANDLE hThread = CreateThread(NULL, 0, watek, NULL, 0, &ThreadID);

  SuspendThread(hThread);    

  CONTEXT context;
  context.ContextFlags = CONTEXT_INTEGER;

  GetThreadContext(hThread, &context);

  cout << "Eax:" << context.Eax << endl;
  cout << "Ebx:" << context.Ebx << endl;
  cout << "Ecx:" << context.Ecx << endl;
  cout << "Edx:" << context.Edx << endl << endl;

  ResumeThread(hThread);

  getch();

  return 0;
}

•      Jak widać, jest to aplikacja CUI.
•      Zacznijmy analizę naszej aplikacji od początku. Najpierw napisaliśmy funkcję stanowiącą punkt wejścia nowego wątku. Nie robi ona nic szczególnego poza wyświetlaniem napisu "To jest nowy watek". To właśnie ten wątek i jego kontekst stanie się obiektem naszych badań.
•      Za pomocą funkcji CreateThread wywołujemy nasz nowy wątek (więcej szczegółów w tekście Wątki - wprowadzenie ). Jego uchwyt jest przechowywany w zmiennej hThread.
•      Zawieszamy teraz nasz nowy wątek za pomocą funkcji SuspendThread. Pobiera ona tylko jeden argument, czyli uchwyt obiektu-wątku. Jest to zalecany zabieg przed pobraniem informacji o kontekście wątku, ponieważ podczas wykonywania się wątku, wartości rejstrów mogą się zmieniać. Później (po pobraniu informacji o kontekście) wznowimy wątek za pomocą funkcji ResumeThread. Ona również pobiera tylko jeden argument - uchwyt obiektu-wątku.
•      Teraz najważniejsza część, czyli porcja kodu odpowiedzialna za odczyt kontekstu naszego nowego wątku zaraz po zawieszeniu. Na początek musimy zaalokować strukturę CONTEXT, a następnie zainicjalizować odpowiednią flagę wskazującą na rejestry, których wartości chcemy odczytać. My podaliśmy CONTEXT_INTEGER, co pozwoli nam odczytać wartości rejestrów całkowitych CPU (Eax, Ebx, Ecx, itd.). Wartości tych rejestrów zostaną zapisane w odpowiednich składowych struktury CONTEXT, ale dopiero po wywołaniu funkcji GetThreadContext. Oto jest jej prototyp:


BOOL GetThreadContext (HANDLE hThread,  PCONTEXT pContext);


Jej parametry są dziecinnie proste do zrozumiena. hThread wskazuje na uchwyt obiektu-wątku, do którego badany kontekst należy, natomiast pContext określawskaźnik do zainicjowanej struktury CONTEXT.
•      Nie pozostaje nic innego jak wyświetlić zawartości rejestrów całkowitych w momencie jego zawieszenia. Teraz możemy wznowić wykonanie naszego wątku za pomocą funkcji ResumeThread.

Kilka słów o zawieszaniu wątków. Funkcja zawieszająca wątek przydaje się podczas pobierania i modyfikacji kontekstu wątku, ponieważ wartości rejestrów zmieniają się podczas wykonywania kodu. Jeżeli chcemy pobrać wartości rejestró w określonym momencie wykonywania, to siłą rzeczy musimy zawiesić ten wątek. W przeciwnym razie wartości, które otrzymamy, mogą  nie zgadzać się z tym co chcieliśmy otrzymać.

Oprócz funkcji GetThreadContext istnieje funkcja, która pozwala na modyfikację kontekstu wątku, czyli SetThreadContext. Oto jej prototyp:


BOOL SetThreadContext( HANDLE hThread,  CONST CONTEXT *pContext);


Argumenty są takie same jak w funkcji GetThreadContext, czyli uchwyt obiektu-wątku i wskaźnik do struktury CONTEXT, którą musimy wcześniej zainicjalizować i ustawić odpowiednią wartość składowej ContextFlags.

Oto przykładowy kod, który zmodyfikuje kontekst utworzonego wątku i umieści wartość 10 w rejestrze Eip:

contextB.cpp

Kod:

#include <iostream>
#include <windows.h>
#include <conio.h>

using namespace std;

DWORD WINAPI watek(PVOID pvParam)
{
  cout << "Ten napis sie nie wyswietli" << endl;
  return 0;
}

int main()
{
 DWORD id;
 HANDLE hThread = CreateThread(NULL, 0, watek, NULL, 0, &id);

/* ------ MODYFIKACJA WARTOSCI EIP (DLA CPU x86) ------ */

CONTEXT context;
context.ContextFlags = CONTEXT_CONTROL;

SuspendThread(hThread);
 
   GetThreadContext(hThread, &context);
   cout << "Eip przed modyfikacja:\t" << context.Eip << endl;

   context.Eip = 10;

   SetThreadContext(hThread, &context);
   cout << "Eip po modyfikacji:\t" << context.Eip << endl;

ResumeThread(hThread);

getch();

return 0;

}

•      Myślę, że powyższy kod powinien być zrozumiały. Dla pewności, w skrócie go omówię.
•      Tworzymy nowy wątek, zawieszamy go i sprawdzamy stan kontekstu przed modyfikacją (a konkretnie wartość rejestru kontrolnego Eip). Ponownie przypominam, że zawieszanie wątku jest konieczne przy odczytywaniu, a szczególnie przy operacjach takich jak modyfikowanie kontekstu. Jest tak, ponieważ zmiana rejestrów podczas wykonywania wątku może doprowadzić do nieprzewidzianych konsekwencji.
•      Następnie definiujemy nową wartość składowej Eip struktury CONTEXT (32-bitowy wskaźnik instrukcji). Przypisujemy jej wartość 10. Pamiętaj, że wcześniej podaliśmy flagę CONTEXT_CONTROL w odpwiedniej składowej, co oznacza, że możemy modyfikować tylko zawartości rejestrów kontrolnych.
•      Wywołujemy teraz funkcję SetThreadContext (opisaną wcześniej). Powoduje to przypisanie nowej wartości (10) dla rejestru kontrolnego Eip w kontekście wątku. Możemy to sprawdzić wywołując ponownie funkcję GetThreadContext. Jak widać, wartość zmieniła się na tą podaną przez nas.
•      Za pomocą funkcji ResumeThread wznowiliśmy wykonanie wątku. Jednak kod napisany wewnątrz funkcji wątkowej nie wykona się (napis "Ten napis sie nie wyswietli" naprawdę się nie wyświetli). Stało się tak, ponieważ zmieniliśmy wartość wskaźnika instrukcji i wykonywanie wątku rozpocznie się od innego adresu niż tego, od którego powinno się rozpocząć.

Operacje na kontekście wątku powinny być przeprowadzane przez osoby, które mają pojęcie o architekturze pamięci w Windows. Bez jej znajomości niczego się nie dokona za pomocą samych funkcji.

Ciekawostką jest fakt, że jeśli ustawimy wartość rejstru Eip na adres, który jest już zajęty przez inny, działający wątek, to nastąpi błąd nieobsłużonego wyjątku i proces, do którego należy ten działający wątek, zakończy się. Nasz jednak będzie ciągle działał.

---------------------------------------------------------------
0x04. Usypianie i przełączanie na inny wątek
---------------------------------------------------------------

Usypianie i przełączanie są metodami kontroli nad  szeregowaniem wątków. Zacznijmy od usypiania. Służy do tego funkcja Sleep:

      VOID Sleep( DWORD dwMilliseconds);

Wątek, który wywołuje tą funkcję zostaje zawieszony na czas określony przez dwMilliseconds (w milisekundach). Dzięki tej funkcji, wątek dobrowolnie oddaje resztę przyznanego mu czasu CPU, a system nie wykonuje go przez mniej więcej tyle czasu ile podaliśmy w argumencie funkcji. Jeśli w argumencie funkcji podaliśmy INFINITE, to ten wątek już w ogóle nie będzie się wykonywał. Jeżeli natomiast pdaliśmy 0, to system odbiera resztę przydzielonego czasu CPU wątkowi, który tą funkcję wywołał i przydziela ten czas innemu wątkowi. (Uwaga: jeżeli nie ma wątków do wykonania o tym samym priorytecie, to system może przydzielić kwant czasu CPU temu samemu wątkowi co wywołał funkcję Sleep(0) ).

Istnieje jeszcze jedna funkcja, która pozwala na przełączanie się na inny wątek. Mowa tu o SwitchToThread, a to jest jej prototyp:

      BOOL SwitchToThread();

Jeśli istnieje wątek oczekujący na przydział CPU, to po wywołaniu tej funkcji, system natychmiast przydziela mu kwant czasu CPU. Ciekawostką jest, że ta funkcja pozwala wykonywać się wątkom nawet o niższym priorytecie niż ten, który wywołał SwitchToThread.

---------------------------------------------------------------
0x05. Priorytety - teoria
---------------------------------------------------------------

Omawiane dotąd pojęcia dotyczyły wątków o takim samym priorytecie. W rzeczywistości wątki mogą mieć różne priorytety i to one mają duży wpływ na kolejność wykonywania się wątków.

Każdy wątek ma przypisany tzw. bazowy poziom priorytetu (nazywany dalej po prostu priorytetem) będący liczbą z przedziału od 0 do 31. 0 oznacza najniższy priorytet, a 31 najwyższy. Algorytm szeregujący wątki w Windows i przydzielający im kwant czasu CPU działa tak, że najpierw wykonują się wątki o wyższym priorytecie. Oznacza to, że dopóki istnieją i są zaszeregowane do wykonania wątki o priorytecie np. 30 to system nie przydzieli dostępu do CPU wątkom o niższym priorytecie (właściwie to istnieje taka możliwość, ale o tym później). Należy pamiętać, że system Windows jest systemem z wywłaszczeniami, a wątki mogą zmieniać swoje priorytety. W praktyce wygląda to tak: Załóżmy, że w danym momencie wykonuje się wątek o priorytecie 15 i w pewnej chwili zostaje zaszeregowany do wykonania wątek o priorytecie 20. Następuje wtedy natychmiastowe przerwanie wykonywania wątku o niższym priorytecie i kwant czasu wykonywania CPU zostaje przekazany temu o wyższym priorytecie. Dzieje się tak nawet wtedy, gdy wcześniejszy wątek nie wykorzystał jeszcze swojego przydziału.

Priorytet (bazowy poziom priorytetu) jest określany na podstawie dwóch wartości. Za pomocą klasy priorytetu procesu i względnego priorytetu wątku. Priorytet jest wynikem kombinacji tych dwóch wartości. Klasa priorytetu określa proces, natomiast względny priorytet wątku określa wątek i jest on ustawiany względem jego procesu. Wynikiem jest oczywiście bazowy poziom priorytetu (priorytet).

W systemie Windows (od Windows 2000) istnieje sześć klas priorytetów o następujących właściwościach:

•      Czasu rzeczywistego - wątki muszą natychmiast reagować na zdarzenia. Wywłaszczają one nawet skladowe systemu operacyjnego, dlatego należy być bardzo ostrożnym podczas korzystania z tej klasy.
•      Wysoki - wątki procesu muszą natychmiast reagować na zdarzenia. Do tej klasy zalicza się Eksplorator Windows czy Menadżer zadań.
•      Powyżej normalnego - priorytet między "normalny" i "wysoki". (Dopiero w Windows 2000).
•      Normalny - standard w systemie Windows. Jeżeli tworzymy nowy proces lub wątek bez określonej wcześniej klasy priorytetu to standardowo, klasa priorytetu tych obiektów jest ustawiona na "normalny".
•      Poniżej normalnego - priorytet między "normalny" i "niski". (Dopiero w Windows 2000).
•      Niski - wątki takiego procesu wykonują się w momencie bezczynności systemu. Przykładem jest wygaszacz ekranu.

Pamiętajmy, że klasa priorytetu określa procesy, a nie wątki. Zajmijmy się teraz kolejną "składową" określającą priorytet, czyli względny priorytet wątku. System Windows udostępnia siedem względnych priorytetów:

•      Krytyczny
•      Najlepszy
•      Powyżej normalnego
•      Normalny
•      Poniżej normalnego
•      Najgorszy
•      Niski.

Aby ustalić właściwy priorytet wątku (od 0 do 31) należy określić zarówno klasę priorytetu procesu i względny priorytet wątku. Oto tabela współzależności:

Względny                                                               Klasa priorytetu procesu.
priorytet             ----------------------------------------------------------------  ----------------------------
wątku.                |  niski    poniżej norm.   normalny     powyżej norm.   wysoki      czasu rzecz.
----------------------------------------------------------------  -------------------------------------------------                         
Krytyczny                15            15                     15               15                 15               31
                       
Najlepszy                6               8                      10               12                 15               26
                       
Pow. norm.              5              7                       9                 11                 14              25
                       
Normalny                4               6                       8                10                 13               24
                       
Pon. norm.             3               5                        7                9                   12               23
                       
Najgorszy               2               4                       6                  8                    11              22
                       
Niski                     1               1                        1                  1                     1              16
----------------------------------------------------------------  -------------------------------------------------

Oznacza to, że aby ustawić priorytet wątku (bazowy poziom) na 12, można np. ustawić klasę priorytetu procesu na "powyżej normalnego", a względny priorytet wątku na "najlepszy".

---------------------------------------------------------------
0x06. Priorytety - praktyka
---------------------------------------------------------------

Zacznijmy od ustawiania klasy priorytetu procesu. Istnieją dwie metody jej ustawienia. Pierwsza z nich to wywołanie nowego procesu za pomocą funkcji CreateProcess (patrz: Współużytkowanie obiektów jądra w systemie Windows ) i podanie odpowiedniej flagi w paramtrze fdwCreate. Oto spis tych flag:

•      REALTIME_PRIORITY_CLASS - czasu rzeczywistego.
•      HIGH_PRIORITY_CLASS - wysoki.
•      ABOVE_NORMAL_PRIORITY_CLASS - powyżej normalnego.
•      NORMAL_PRIORITY_CLASS - normalny.
•      BELOW_NORMAL_PRIORITY_CLASS - poniżej normalnego.
•      IDLE_PRIORITY_CLASS - niski.

Drugim sposobem określenia klasy priorytetu procesu jest funkcja SetPriorityClass, a oto jest jej prototyp:

          BOOL SetPriorityClass( HANDLE hProcess, DWORD fdwPriority);

Pierwszy parametr określa uchwyt procesu, którego klasę chcemy zmienić, natomiast drugi przekazuje jedną z flag podanych powyżej. Istnieje jeszcze funkjca, która pozwala sprawdzić klasę priorytetu procesu tj.:

            DWORD GetPriorityClass( HANDLE hProcess);

Funkcja ta pobiera uchwyt obiektu-procesu, a zwraca jeden z identyfikatorów podamych powyżej

Przejdźmy teraz do ustawienia względnego priorytetu wątku. Tym razem mamy tylko jedną możliwość, a mianowicie funkcję SetThreadPriority. Prototyp:

            BOOL SetThreadPriority( HANDLE hThread, int nPriority);

Parametr hThread przekazuje oczywiście uchwyt wątku, którego priorytet chcemy zmienić. Parametr nPriority przekazuje identyfikator określający jeden z priorytetów. Oto lista:

•      THREAD_PRIORITY_TIME_CRITICAL - krytyczny.
•      THREAD_PRIORITY_HIGHEST - najlepszy.
•      THREAD_PRIORITY_ABOVE_NORMAL - powyżej normalnego.
•      THREAD_PRIORITY_NORMAL - normalny.
•      THREAD_PRIORITY_BELOW_NORMAL - poniżej normalnego.
•      THREAD_PRIORITY_LOWEST - najgorszy.
•      THREAD_PRIORITY_IDLE - niski.

Istnieje jeszcze funkcja int GetThreadPriority( HANDLE hThread), która pozwala sprawdzić względny priorytet wątku. Jako argument podajemy uchwyt badanego wątku, a funkcja zwraca jeden z identyfikatorów podanych powyżej.

Poniższy przykład pokazuje w jaki sposób można ustawić bazowy poziom priorytetu (priorytet) dla wątków i jaki ma to wpływ na ich wykonanie:

priorytet.cpp

Kod:

#include <iostream>
#include <windows.h>
#include <conio.h>

using namespace std;


DWORD WINAPI watek_1(PVOID pvParam)
{
     cout << "To jest watek numer 1\n";
     return 0;
}


DWORD WINAPI watek_2(PVOID pvParam)
{
     cout << "To jest watek numer 2\n";
     return 0;
}


int main()
{
   HANDLE proces = GetCurrentProcess();
   SetPriorityClass(proces, ABOVE_NORMAL_PRIORITY_CLASS);
   
   DWORD watek1;
   HANDLE hWatek_1 = CreateThread(NULL, 0, watek_1, NULL, 0, &watek1);
 
   SuspendThread(hWatek_1);
        SetThreadPriority(hWatek_1, THREAD_PRIORITY_NORMAL); //10  
   ResumeThread(hWatek_1);
   
   
   DWORD watek2;
   HANDLE hWatek_2 = CreateThread(NULL, 0, watek_2, NULL, 0, &watek2);
   SuspendThread(hWatek_2);
        SetThreadPriority(hWatek_2, THREAD_PRIORITY_TIME_CRITICAL);  //15
   ResumeThread(hWatek_2);
     
   HANDLE watek_glowny = GetCurrentThread();
   SetThreadPriority(watek_glowny, THREAD_PRIORITY_LOWEST);
   
   cout << "To jest watek glowny\n";
   
   getch();
   
   return 0;
   
}

•      Powyższy program składa się trzech wątków. Jednego głównego, który m.in. wyświetla napis "To jest watek glowny" i dwóch utworzonych przez nas. One również wyświetlają napisy. W naszym przypadku wątki mają różne priorytety (bazowe poziomy priorytetów). watek_1 ma priorytet 10, watek_2 ma 15, a wątek główny posiada priorytet 8 (ustawione zgodnie z tabelką podaną wcześniej). Dzięki priorytetom możemy kontrolować kolejność pojawiana się napisów. W naszym przypadku najpierw pojawi się napis "To jest watek numer 2" (priorytet 15), później "To jest watek numer 1" (priorytet 10) i na końcu "To jest watek glowny" (priorytet 8).
•      Przeanalizujmy teraz dokładnie kod źródłowy aplikacji. Na początku mamy dwie funkcje wątkowe i każda z nich wyświetla inny napis.
•      Przejdźmy teraz do funkcji main. Na początek ustawiamy względną klasę priorytetu naszego całego procesu. Odpowiada za to ten fragment kodu:

           HANDLE proces = GetCurrentProcess();     
          SetPriorityClass(proces, ABOVE_NORMAL_PRIORITY_CLASS);

Wpierw przekazujemy do zmiennej proces uchwyt (a właściwie pseudouchwyt) naszego procesu. Funkcja GetCurrentProcess zwraca pseudouchwyt obiektu-procesu, w którym została uruchomiona. Następnie ustawiamy klasę priorytetu procesu na "powyżej normalnego" za pomocą funkcji SetPriorityClass opisanej wcześniej. Teraz nadszedł czas na utworzenie nowych wątków i ustawienie ich względnych priorytetów.
•      To jest fragment kodu odpowiedzialny za utworzenie pierwszego z wątków i ustawienie jego względnego priorytetu (i co za tym idzie bazowego poziomu priorytetu):

DWORD watek1;
HANDLE hWatek_1 = CreateThread(NULL, 0, watek_1, NULL, 0, &watek1);
SuspendThread(hWatek_1);
  SetThreadPriority(hWatek_1, THREAD_PRIORITY_NORMAL);
ResumeThread(hWatek_1);

Najpierw tworzymy wątek za pomocą funkcji CreateThread. Później zawieszamy go funkcją SuspendThread, ustawiamy względny priorytet wątku na "normalny" i wątek wznawiamy. Po wykonaniu się tego fragmentu kodu, priorytet wątku wynosi 10. Analogicznie postępujemy z wątkiem drugim, z tą różnicą, że priorytet wątku ustawiamy na 15.
•      Teraz ustawiamy priorytet wątku głównego za pomocą dwóch linijek kodu:
                   
         HANDLE watek_glowny = GetCurrentThread();
         SetThreadPriority(watek_glowny, THREAD_PRIORITY_LOWEST);

Po tym zabiegu, priorytet wątku głównego będzie wynosił 8. Na początek wywołujemy funkcję GetCurrentThread, która przekazuje do zmiennej watek_glowny pseudouchwyt bieżącego wątku (w tym przypadku wątku głównego). Następnie, względny priorytet wątku głównego ustawiamy na "najgorszy" za pomocą funkcji SetThreadPriority.

Myślę, że powyższy kod był łatwy do zrozumienia. Zachęcam do eksperymentowania z wątkami poprzez zmianę odpowiednich flag w odpowiednich funkcjach.

---------------------------------------------------------------
0x07. Dynamiczne podwyższanie priorytetów
---------------------------------------------------------------

Jak zostało napisane wcześniej, system ustala poziom priorytetu na podstawie względnego priorytetu wątku i klasy priorytetu procesu tego wątku. Zdarza się, że system musi podwyższyć bazowy poziom priorytetu wątku - zazwyczaj w odpowiedzi na jakieś zdarzenie np. Na wciśnięcie klawisza.

Wyobraźmy sobie, że jakiś wątek ma bazowy poziom priorytetu ustawiony na 13. Wątek ten reaguje na wciśnięcie dowolnego klawisza. Załóżmy, że użytkownik wcisnął jakiś klawisz. W tym przypadku może się zdarzyć, że terownik klawiatury każe systemowi podwyższyć chwilowo poziom priorytetu wątku. Jeśli nastąpi np.zwiększenie poziomu o 2, to nasz wątek zostaje zaszeregowany do wykonania z priorytetem 15 przez jeden kwant czasu. Po jego upłynięciu, system zmniejsza priorytet wątku o 1 i przez następny kwant czasu wynosi on 14. W kolejnym kwancie czasu znowu nastąpi dekrementacja i tym razem priorytet spada do swojego poziomu bazowego (13). Przy natępnych kwantach czasu poziom priorytetu będzie nadal wynosił 13. Oznacza to, że poziom priorytetu wątku nigdy nie spada poniżej swojego bazowego poziomu. Należy zauważyć, że nie mamy wpływu na to o ile podwyższy się poziom priorytetu.
System może podwyższać priorytety tylko tych wątków, których bazowy poziom mieści się w przedziale od 1 do 15. Do tego priorytet nie może być podwyższany (dynamicznie) do zakresu czasu rzeczywistego (powyżej 15).

Programiści mają jednak możliwośc zablokowania dynamicznego podwyższania priorytetów w swoich aplikacjach:

      BOOL SetProcessPriorityBoost( HANDLE hProcess, BOOL DisablePriorityBoost);

Funkcja każe systemowi włączyć lub wyłączyć podwyższanie priorytetów wszystkich wątków danego procesu. Parametry są chyba jasne. Aby zablokować mechanizm podwyższania priorytetów w procesie o uchwycie hProcess i wystarczy ustawić w drugim parametrze wartość TRUE.

Możemy również wyłączyć ten mechanizm tylko dla wskazanego wątku, za pomocą funkcji:

       BOOL SetThreadPriorityBoost( HANDLE hThread, BOOL DisablePriorityBoost);

Istnieją jeszcze dwie funkcje, które pozwalają nam sprawdzić, czy mechanizm podwyższania priorytetów jest aktywny:

- dla procesu:
                 
      BOOL GetProcessPriorityBoost(HANDLE hProcess, PBOOL pDisablePriorityBoost);

- dla wątku: 

       BOOL GetThreadPriorityBoost(HANDLE hThread, PBOOL pDisablePriorityBoost);

Do każdej z tych funkcji przekazuje się uchwyt sprawdzanego procesu lub wątku oraz adres zmiennej typu BOOL, której wartość zostanie odpowiednio ustawiona przez funkcję.

Na zakończenie wspomnę jeszcze, że system może podwyższyć poziom priorytetu wątku w następującej sytuacji: Przyjmijmy, że istnieje wątek o priorytecie 4, który jest gotowy do wykonania, ale nie może uzyskać dostępu do CPU, ponieważ cały czas wykonuje wię wątek o priorytecie 8. Jeżeli system wykryje, że jakiś wątek jest w ten sposób zablokowany przez dłuższy czas , to dynamicznie podwyższa jego priorytet do 15 i pozwala mu wykonywać się przez dwa kolejne kwanty czasu. Gdy one upłynął, wątek powraca do swojego bazowego poziomu ustawień. Oto kod, który pokazuje opisaną sytuację:

priorytet2.cpp

Kod:

#include <iostream>
#include <windows.h>
#include <conio.h>

using namespace std;


DWORD WINAPI watek_1(PVOID pvParam)
{
     cout << "To jest watek numer 1\n";
     return 0;
}


int main()
{
   HANDLE proces = GetCurrentProcess();
   SetPriorityClass(proces, ABOVE_NORMAL_PRIORITY_CLASS);
   
   DWORD watek1;
   HANDLE hWatek_1 = CreateThread(NULL, 0, watek_1, NULL, 0, &watek1);
   
   SuspendThread(hWatek_1);
       SetThreadPriority(hWatek_1, THREAD_PRIORITY_IDLE);  //1
   ResumeThread(hWatek_1);
   
     
   HANDLE watek_glowny = GetCurrentThread();
   SetThreadPriority(watek_glowny, THREAD_PRIORITY_LOWEST);
   
  while(1){}
   
   getch();
   
   return 0;
   
}

•      Jak wiadomo, jeśli sami nie zablokowaliśmy dynamicznego podwyższania priorytetów, to mechanizm jest aktywny standardowo. W tym wypadku mamy jeden wątek o priorytecie 1 i wątek główny 8. Po utworzeniu nowego wątku z priorytetem 1 i ustawieniu swojego priorytetu na 8, wątek główny wchodzi w nieskończoną pętlę. Zgodnie z tym co pisałem w rozdziale "Priorytety - teoria" wątek potomny nie powinien się wykonać, ponieważ przez cały czas działa wątek główny z wyższym priorytetem. Tutaj jednak następuje mechanizm opisany przed chwilą. Po kilku sekundach wyświetli się napis "To jest watek numer 1", ponieważ system podwyższy priorytet zablokowanego wątku, aby ten mógł się wykonać.
•      Oczywiście aby to się stało, mechanizm podwyższania priorytetów musi być włączony (tak jest standardowo).

---------------------------------------------------------------
0x08. Bibliografia
---------------------------------------------------------------

J. Richter, Programowanie aplikacji dla Windows, Warszawa 2002

msdn.microsoft.com

Offline

 
  • Index
  •  » Tutorial
  •  » [C/C++]Wątki - kontekst, szeregowanie i priorytety

Stopka forum

RSS
Powered by PunBB 1.2.23
© Copyright 2002–2008 PunBB
Polityka cookies - Wersja Lo-Fi


Darmowe Forum | Ciekawe Fora | Darmowe Fora
GotLink.pl