Wyjście Spis treści Poprzedni Następny
Autor:
©Iczelion |
©2008 mgr
Jerzy Wałaszek |
|
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}.
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:
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.
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 } }
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.
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. |
I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe