Rozdział XV - Programowanie Wielowątkowe


Na tej lekcji dowiemy się, jak stworzyć program wielowątkowy. Przestudiujemy również sposoby komunikacji pomiędzy wątkami.


Pobierz plik z przykładem {z tego archiwum}.

 

Teoria

Na poprzedniej lekcji nauczyłeś się, iż proces składa się przynajmniej z jednego wątku: wątku głównego. Wątek jest łańcuchem wykonań rozkazów zawartych w programie. Możesz również w swoim programie utworzyć dodatkowe wątki. Wielowątkowość (multithreading) możesz potraktować jako wielozadaniowość (multitasking) w obrębie jednego programu. W sensie implementacji wątek jest pewną funkcją, która wykonuje się równolegle z głównym programem. Możesz uruchamiać kilka egzemplarzy tej samej funkcji lub możesz uruchomić kilka różnych funkcji jednocześnie w zależności od swoich wymagań. Wielowątkowość jest charakterystyczna dla biblioteki Win32, w Win16 brak jest jej odpowiednika.

Wątki działają w obrębie tego samego procesu, zatem posiadają one dostęp do każdego zasobu w tym procesie, jak zmienne globalne, uchwyty itp. Jednakże każdy wątek posiada swój własny stos, więc zmienne lokalne w każdym wątku są dla niego prywatne. Każdy wątek posiada również swój prywatny zestaw rejestrów, zatem gdy system Windows przełącza wykonanie do innych wątków, mogą one "zapamiętać" swój ostatni stan i "wznowić" zadanie, gdy z powrotem otrzymają kontrolę nad procesorem. Obsługiwane jest to wewnętrznie przez system Windows.

Wątki możemy podzielić na dwie kategorie:

  1. Wątek interfejsu użytkownika: ten typ wątku tworzy swoje własne okno, zatem otrzymuje wiadomości z systemu Windows. Może współpracować z użytkownikiem poprzez swoje własne okna, stąd pochodzi jego nazwa. Wątek tego rodzaju jest podmiotem reguły Mutexu z Win16, która zezwala tylko na pojedynczy wątek interfejsu użytkownika w 16 bitowym jądrze graficznym. Gdy tego rodzaju wątek interfejsu użytkownika wykonuje kod w obrębie 16-bitowego jądra graficznego, inne wątki interfejsu użytkownika nie mogą korzystać z usług tego jądra. Sytuacja ta jest charakterystyczna dla Windows 95, ponieważ pod maską system ten zawiera 16-bitowe jądro. W systemie Windows NT/2000/XP nie istnieje Mutex z Win16, zatem wątki interfejsu użytkownika działają znacznie lepiej niż w Windows 95 (który to system i tak jest już historią).
  2. Wątek roboczy: ten typ wątku nie tworzy okna, zatem nie może odbierać żadnych wiadomości z systemu Windows. Istnieje głównie po to, aby wykonywać przydzieloną mu pracę w tle innych procesów, stąd nazwa wątek roboczy.

Proponuję następującą strategię przy korzystaniu z wielowątkowości w Win32: Niech wątek główny obsługuje interfejs użytkownika, a inne wątki niech wykonują czarną robotę w tle. W ten sposób wątek główny staje się jakby Prezydentem, a inne wątki to jego gabinet. Prezydent wyznacza prace dla swojego gabinetu utrzymując stały kontakt z wyborcami. Gabinet prezydencki posłusznie wykonuje nakazaną pracę przekazując jej wyniki Prezydentowi. Jeśliby prezydent miał wszystko robić sam, nie byłby w stanie zwracać należytej uwagi na wyborców lub prasę. Dzieje się tak z oknem, które zajęte jest wykonywaniem długiego zadania w swoim wątku głównym i nie reaguje na działania użytkownika, aż zakończy tę pracę. Taki program może zyskać na utworzeniu dodatkowego wątku, który będzie odpowiedzialny za długotrwałe zadanie, pozwalając głównemu wątkowi reagować na polecenia od użytkownika.

Wątek możemy utworzyć wywołując funkcję CreateThread o następującej składni:


CreateThread PROTO lpThreadAttributes: DWORD,\ 
                   dwStackSize:        DWORD,\ 
                   lpStartAddress:     DWORD,\ 
                   lpParameter:        DWORD,\ 
                   dwCreationFlags:    DWORD,\ 
                   lpThreadId:         DWORD 


Funkcja CreateThread wygląda bardzo podobnie do CreateProcess.

Jeśli wywołanie CreateThread się powiedzie, zwróci ono uchwyt do nowo utworzonego wątku. W przeciwnym wypadku zwracana jest wartość NULL.

Funkcja wątku uruchomi się natychmiast po wywołaniu CreateThread, chyba że określisz znacznik CREATE_SUSPENDED w parametrze dwCreationFlags. W takim przypadku wątek zostanie zawieszony aż do wywołania funkcji ResumeThread.

Gdy funkcja wątku wraca za pomocą instrukcji ret, system Windows wywołuje niejawnie dla tego wątku funkcję ExitThread. Możesz wywołać tę funkcję z wnętrza swojego wątku, lecz nie ma to specjalnie sensu. Kod wyjścia z wątku możesz z łatwością pobrać za pomocą wywołania funkcji GetExitCodeThread.

Jeśli chcesz zakończyć jakiś wątek z wnętrza innego wątku, możesz wywołać funkcję TerminateThread. Jednakże powinieneś to robić tylko w ostateczności, ponieważ funkcja TerminateThread zamyka dany wątek natychmiast nie dając mu szansy posprzątania po sobie (zamknięcia plików, zwolnienia używanych zasobów, itp.).

Teraz przejdźmy do metod wymiany informacji pomiędzy wątkami. Istnieją trzy takie metody:

Wątki dzielą zasoby procesu łącznie ze zmiennymi globalnymi, zatem mogą one wykorzystać zmienne globalne do komunikacji pomiędzy sobą. Jednakże metoda taka musi być stosowana z rozwagą. Należy przemyśleć synchronizację wątków. Na przykład jeśli dwa wątki używają tej samej struktury o 10 polach, to co się stanie, gdy system Windows nagle odbierze wątkowi sterowanie w trakcie uaktualniania tej struktury? Drugi wątek otrzyma niespójne dane w strukturze! Nie łudź się, programy wielowątkowe są bardzo trudne w uruchamianiu i w wyłapywaniu błędów. Ten typ błędu zdarza się w przypadkowych okresach czasu, zatem jest trudny do wyśledzenia.

Możesz również wykorzystać wiadomości Windows do komunikacji pomiędzy wątkami. Jeśli wątki są wątkami interfejsu użytkownika, to nie ma problemu: metoda ta może być wykorzystana do obustronnej komunikacji. Musisz jedynie zdefiniować jedną lub więcej prywatnych wiadomości, które mają znaczenie dla tych wątków. Prywatne wiadomości definiujesz za pomocą wiadomości WM_USER, która stanie się wartością bazową, co możesz wykonać w sposób następujący:


WM_MOJA_WIADOMOSC EQU WM_USER+100h


System Windows
nie będzie używał wartości począwszy od WM_USER w górę dla własnych wiadomości, zatem możesz wykorzystać wartość WM_USER i powyżej dla swoich celów

Jeśli jeden z wątków jest wątkiem interfejsu użytkownika a drugi wątkiem roboczym, to nie możesz zastosować opisanej powyżej metody do obustronnej komunikacji, ponieważ wątek roboczy nie posiada własnego okna, a zatem nie posiada kolejki wiadomości. Możesz zastosować następujący schemat:


Wątek Interfejsu Użytkownika → zmienne globalne → Wątek Roboczy
Wątek Roboczy → prywatne wiadomości → Wątek Interfejsu Użytkownika

 


W naszym przykładzie korzystamy właśnie z tej metody.

Ostatnią metodą komunikacji jest obiekt zdarzenia (event object). Możesz go traktować jako pewnego rodzaju znacznik. Jeśli obiekt zdarzenia jest w stanie "nie zasygnalizowanym", wątek znajduje się w stanie uśpienia, w którym nie otrzymuje czasu procesora. Gdy obiekt zdarzenia jest w stanie "zasygnalizowanym", system Windows "budzi" wątek i zaczyna on wykonywać przydzielone mu zadanie.

 

Przykład

Powinieneś pobrać przykładowe archiwum zip, rozpakować je i uruchomić program thread1.exe.


 

Kliknij opcję menu Dzikie Obliczenia. Nakaże to programowi wykonać instrukcję add eax, eax 600'000'000 razy. Zwróć uwagę, iż w tym czasie nie możesz nic zrobić z głównym oknem: nie możesz go przesunąć, wybrać coś z menu, itp. Gdy obliczenia się zakończą, pojawi się okienko wiadomości. Wtedy okno zacznie normalnie reagować na twoje polecenia.

Aby zaoszczędzić użytkownikowi tego typu niewygód, możemy przenieść procedurę "obliczeń" do oddzielnego wątku roboczego, a głównemu wątkowi pozwolić zajmować się zadaniami interfejsu użytkownika. Możesz zauważyć, że chociaż okno główne reaguje wolniej niż zazwyczaj, to jednak ciągle reaguje.

Plik THREAD.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\user32.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDELIB \masm32\lib\user32.lib
INCLUDELIB \masm32\lib\kernel32.lib

.CONST

IDM_CREATE_THREAD EQU 1
IDM_EXIT          EQU 2
WM_FINISH         EQU WM_USER+100h

.DATA

ClassName DB "Win32ASMThreadClass", 0
AppName   DB "Program wielowątkowy w asemblerze", 0
MenuName  DB "FirstMenu", 0

.DATA?

hInstance   HINSTANCE ?
CommandLine LPSTR     ?
hMenu       HANDLE    ?
ExitCode    DWORD     ?
hwnd        HANDLE    ?
ThreadID    DWORD     ?

.CODE

start:
    INVOKE GetModuleHandle, NULL
    mov    hInstance, eax
    INVOKE GetCommandLine
    mov    CommandLine, eax
    INVOKE WinMain, hInstance, NULL, CommandLine, SW_SHOWDEFAULT
    INVOKE ExitProcess, eax

WinMain PROC hInst:     HINSTANCE,\
             hPrevInst: HINSTANCE,\
             CmdLine:   LPSTR,\
             CmdShow:   DWORD

LOCAL wc  : WNDCLASSEX
LOCAL msg : MSG

    mov    wc.cbSize, SIZEOF WNDCLASSEX
    mov    wc.style, CS_HREDRAW OR CS_VREDRAW
    mov    wc.lpfnWndProc, OFFSET WndProc
    mov    wc.cbClsExtra, NULL
    mov    wc.cbWndExtra, NULL
    push   hInst
    pop    wc.hInstance
    mov    wc.hbrBackground, COLOR_WINDOW+1
    mov    wc.lpszMenuName, OFFSET MenuName
    mov    wc.lpszClassName, OFFSET ClassName
    INVOKE LoadIcon, NULL, IDI_APPLICATION
    mov    wc.hIcon, eax
    mov    wc.hIconSm, eax
    INVOKE LoadCursor, NULL, IDC_ARROW
    mov    wc.hCursor, eax
    INVOKE RegisterClassEx, ADDR wc
    INVOKE CreateWindowEx, WS_EX_CLIENTEDGE,\
                           ADDR ClassName, ADDR AppName,\
                           WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,\
                           CW_USEDEFAULT, 300, 200, NULL, NULL,\
                           hInst, NULL
    mov    hwnd, eax
    INVOKE ShowWindow, hwnd, SW_SHOWNORMAL
    INVOKE UpdateWindow, hwnd
    INVOKE GetMenu, hwnd
    mov    hMenu, eax

    .WHILE TRUE
        INVOKE GetMessage, ADDR msg, NULL, 0, 0
        .BREAK .IF (!eax)
        INVOKE TranslateMessage, ADDR msg
        INVOKE DispatchMessage, ADDR msg
    .ENDW

    mov eax, msg.wParam
    ret

WinMain ENDP

WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    .IF uMsg==WM_DESTROY
        INVOKE PostQuitMessage, NULL
    .ELSEIF uMsg==WM_COMMAND
        mov eax, wParam
        .IF lParam==0
            .IF ax==IDM_CREATE_THREAD
                mov    eax, OFFSET ThreadProc
                INVOKE CreateThread, NULL, NULL, eax,\
                                     NULL, NORMAL_PRIORITY_CLASS,\
                                     ADDR ThreadID
                INVOKE CloseHandle, eax
            .ELSE
                INVOKE DestroyWindow, hWnd
            .ENDIF
        .ENDIF
    .ELSEIF uMsg==WM_FINISH
        INVOKE MessageBox, NULL, ADDR AppName, ADDR AppName, MB_OK
    .ELSE
        INVOKE DefWindowProc, hWnd, uMsg, wParam, lParam
        ret
    .ENDIF

    xor eax, eax
    ret

WndProc ENDP

ThreadProc PROC USES ecx Param:DWORD

    mov    ecx, 600000000
Loop1:
    add    eax, eax
    loop   Loop1
    INVOKE SendMessage, hwnd, WM_FINISH, NULL, NULL
    ret

ThreadProc ENDP

END start

 

Plik THREAD.RC

 

// Stałe dla menu

#define IDM_CREATE_THREAD 1
#define IDM_EXIT          2

FirstMenu MENU
{
POPUP "&Proces"
    {
         MENUITEM "&Twórz Wątek", IDM_CREATE_THREAD
         MENUITEM SEPARATOR
         MENUITEM "&Koniec", IDM_EXIT
    }
}

Analiza

Program główny prezentuje użytkownikowi normalne okno wyposażone w menu. Jeśli użytkownik wybierze opcję menu Twórz Wątek, to program utworzy go następująco:


    .IF ax==IDM_CREATE_THREAD
        mov    eax, OFFSET ThreadProc
        INVOKE CreateThread, NULL, NULL, eax,\
                             NULL, NORMAL_PRIORITY_CLASS,\
                             ADDR ThreadID
        INVOKE CloseHandle, eax


Powyższa funkcja tworzy wątek, który uruchomi procedurę nazwana ThreadProc współbieżnie z wątkiem głównym. Jeśli wywołanie się powiedzie, CreateThread powraca natychmiast, a ThreadProc zaczyna się wykonywać. Ponieważ nie używamy uchwytu wątku, powinniśmy go zamknąć, w przeciwnym wypadku będzie pewien wyciek pamięci. Zwróć uwagę, iż zamknięcie uchwytu wątku nie powoduje zamknięcia samego wątku. Jedynym skutkiem tej operacji jest to, iż już więcej nie będziemy mogli z tego uchwytu skorzystać.


ThreadProc PROC USES ecx Param:DWORD

    mov    ecx, 600000000
Loop1:
    add    eax, eax
    loop   Loop1
    INVOKE SendMessage, hwnd, WM_FINISH, NULL, NULL
    ret

ThreadProc ENDP


Procedura ThreadProc wykonuje dzikie obliczenia, które zabierają całkiem sporo czasu, a gdy je zakończy wysyła wiadomość WM_FINISH do głównego okna. WM_FINISH jest naszą prywatną wiadomością zdefiniowaną następująco:


WM_FINISH EQU WM_USER+100h


Nie musisz do wartości WM_USER dodawać 100h, lecz tak jest bezpieczniej.

Wiadomość WM_FINISH ma znaczenie tylko w naszym programie. Gdy okno główne otrzyma tę wiadomość, reaguje wyświetleniem okna z informacją o zakończeniu obliczeń.

Możesz utworzyć kilka kolejnych wątków wybierając kilkakrotnie pod rząd element menu Twórz Wątek.

W naszym przykładzie komunikacja jest jednostronna, tzn. tylko watek może powiadomić okno główne. Jeśli chcesz, aby wątek główny wysyłał polecenia do wątku roboczego, to możesz to zrobić następująco:

Gdy użytkownik wybierze opcję menu Zatrzymaj Wątek, program główny ustawi znacznik polecenia na TRUE. Gdy procedura ThreadProc zauważy tę wartość znacznika polecenia, przerwie wykonywanie pętli i powróci kończąc w ten sposób wątek.

 

Dodatek w Pascalu

Ta sama aplikacja w Pascalu: (program korzysta z plików zasobów, który należy umieścić w katalogu projektowym pod nazwą RSRC.RC).

 

{********************************
**  I Liceum Ogólnokształcące  **
**           w Tarnowie        **
**       mgr Jerzy Wałaszek    **
********************************}

program Thread;

uses Windows;

const
  IDM_CREATE_THREAD  = 1;
  IDM_EXIT           = 2;
  WM_FINISH          = WM_USER + $100;
  ClassName          = 'Win32ASMThreadClass';
  AppName            = 'Program wielowątkowy w FreePascalu';
  MenuName           = 'FirstMenu';

var
  hInstance : HINST;
  hMenu     : HANDLE;
  ExitCode  : longword;
  hwnd      : HANDLE;
  ThreadID  : longword;

procedure ThreadProc(Param:DWORD);
var
  i : longword;
begin
  i := 600000000;
  while (i > 0) do i := i - 1;
  SendMessage(hwnd,WM_FINISH,0,0);
end;

function WndProc(hWnd:HANDLE;uMsg:UINT;wParam:WPARAM;lParam:LPARAM) : longint;
begin
  Result := 0;
  case uMsg of
    WM_DESTROY: PostQuitMessage(0);
    WM_COMMAND:
      if lParam = 0 then
      begin
        if (wParam and $ffff) = IDM_CREATE_THREAD then
          CloseHandle(CreateThread(0,0,@ThreadProc,0,NORMAL_PRIORITY_CLASS,ThreadID))
        else DestroyWindow(hWnd);
      end;
    WM_FINISH: MessageBox(0,AppName,AppName,MB_OK);
    else Result := DefWindowProc(hWnd,uMsg,wParam,lParam);
  end;
end;

function WinMain(hInst,hPrevInst:HINST;CmdLine:LPSTR;CmdShow:DWORD) : longint;
var
  wc  : WNDCLASSEX;
  msg : MSG;
begin
  with wc do
  begin
    cbSize        := sizeof(WNDCLASSEX);
    style         := CS_HREDRAW or CS_VREDRAW;
    lpfnWndProc   := @WndProc;
    cbClsExtra    := 0;
    cbWndExtra    := 0;
    hInstance     := hInst;
    hbrBackground := COLOR_WINDOW + 1;
    lpszMenuName  := MenuName;
    lpszClassName := ClassName;
    hIcon         := LoadIcon(0,IDI_APPLICATION);
    hIconSm       := hIcon;
    hCursor       := LoadCursor(0,IDC_ARROW);
  end;
  RegisterClassEx(wc);
  hwnd := CreateWindowEx(WS_EX_CLIENTEDGE,ClassName,AppName,
                         WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,CW_USEDEFAULT,
                         300,200,0,0,hInst,0);
  ShowWindow(hwnd,SW_SHOWNORMAL);
  UpdateWindow(hwnd);
  hMenu := GetMenu(hwnd);
  while GetMessage(msg,0,0,0) do
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
  Result := msg.wParam;
end;

begin
  hInstance := GetModuleHandle(0);
  ExitProcess(WinMain(hInstance,0,GetCommandLine,SW_SHOWDEFAULT));
end.

 

Autorem kursu jest Iczelion. Kurs programowania Windows znalazł się na serwerze I LO w Tarnowie za pisemną zgodą autora.
Tłumaczenie z języka angielskiego, opracowanie HTML i konwersję przykładów programów wykonał mgr Jerzy Wałaszek.



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.