Iczelion - 15 - Multithreading Programmierung

Tutorial 15: Multithreading Programmierung


Wir werden in diesem Tutorial lernen wie ein Multithreading Programm erstellt wird. Außerdem schauen wir uns die Kommunikations-Methoden zwischen den einzelnen Threads an.

Laden Sie das Beispiel hier herunter.

Theorie:

Im vorherigen Tutorial haben Sie gelernt, dass ein Prozess aus mindestens einem Thread besteht: dem Haupt-Thread. Ein Thread ist eine Kette von Befehlen. Sie können auch zusätzlich Threads in ihrem Programm erzeugen. Sie können Multithreadings als Multitasking innerhalb eines Programms ansehen. Im Sinne der Implementation ist ein Thread eine Funktion die parallel zum Hauptprogramm läuft. Sie können verschiedene Instanzen der selben Funktion laufen lassen oder Sie können verschiedene Funktionen simultan, abhängig der Anforderungen, laufen lassen. Multithreading ist Win32 spezifisch, es existiert kein Gegenstück für Win16.

Threads laufen im selben Prozess, so dass sie Zugriff auf jegliche Ressourcen in dem Prozess haben, so wie globale Variablen, Handles, etc. Wie auch immer, jeder Thread hat seinen eigenen Stack, so dass lokale Variablen in jedem Thread private sind. Jeder Thread hat auch seine privaten Register, so dass, wenn Windows zu einem anderen Thread schaltet, kann der Thread sich an seinen letzten Status "erinnern" und kann den Task wieder "aufnehmen" wenn es wieder die Kontrolle erhält. Das wird intern von Handle gehändelt.

Wir können Threads in zwei Kategorien einteilen:

  1. Benutzer-Schnittstellen Thread: Diese Art von Thread erzeugt ein eigenes Fenster, so dass es Windows-Nachrichten entgegennimmt. Es kann auf den Benutzer reagieren via seinem eigenem Fenster, woraus auch der Name resultiert. Diese Art von Thread ist Bestandteil der Win16 Mutex Regel, welche nur eine Benutzerschnittstelle im 16-Bit Benutzer und GDI Kernel erlaubt. Während ein Benutzer-Schnittstellen Thread Code im 16-Bit Benutzer und GDI-Kernel ausführt, können andere Benutzer-Schnittstellen nicht die Dienste des 16-Bit Benutzer und GDI-Kernel nutzen. Beachten Sie, dass dieser Win16 Mutex Win 95 und darunter spezifisch ist. Windows NT hat keinen Win16 Mutex hat keine Benutzer-Schnittstellen Threads, so dass Benutzer-Schnittstellen Threads unter NT wesentlich flüssiger als unter Windows 95 laufen.
  2. Arbeitende Threads: Diese Art von Threads erzeugen keine Fenster, so dass sie keine Windows-Nachrichten erhalten können. Sie existieren hauptsächlich für die zugewiesene Arbeit, die im Hintergrund läuft, daher der Name Arbeitender Thread.
Ich empfehle die folgende Strategie wenn die multithreading Möglichkeiten unter Win32 genutzt werden: Lassen Sie den Hauptthread die Benutzer-Schnittstellen-Dinge tun und die anderen Threads die harte Arbeit im Hintergrund. Auf diese Weise ist der Hauptthread wie ein Gouverneur, andere Threads wie der Gouverneurs Stabs. Der Gouverneur teilt seinem Stab Arbeiten zu, während er hauptsächlich im Kontakt mit der Öffentlichkeit steht. Der Gouverneurs Stab führt gehorsam die Arbeit aus und berichtet dem Gouverneur. Wenn der Gouverneur jede Arbeit selber machen würde, könnte er nicht viel Aufmerksam der Öffentlichkeit oder der Presse schenken. Das ist genauso wie bei einem Fenster, welches damit beschäftigt ist eine längere Aufgabe im Hauptthread zu bearbeiten: es reagiert nicht auf den Benutzer, bis die Aufgabe abgearbeitet wurde. Solche Programme können vom erzeugen eines weiteren Threads, der für die längere Aufgabe zuständig ist, profitieren, damit der Hauptthread auf die Benutzer-Kommandos reagieren kann.

Wir können ein Thread erzeugen, indem wir die CreateThread-Funktion aufrufen, welche folgende Syntax hat:

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


Die CreateThread-Funktion hat Ähnlichkeit mit CreateProcess.
lpThreadAttributes  -- Sie können NULL benutzen, wenn Sie wollen, dass der Thread die Standard-Sicherheiteinstellungen benutzen soll.
dwStackSize -- spezifiziert die Stackgröße des Threads. Wenn Sie dem Tread die selbe Stackgröße wie dem Hauptthread geben wollen, benutzen Sie NULL als Parameter.
lpStartAddress -- Die Addresse der Thread-Funktion. Das ist die Funktion, die die Arbeit des Threads verrichtet. Die Funktion MUSS einen, und nur einen 32-bit Parameter entgegennehmen und einen 32-Bit Wert zurückliefern.
lpParameter -- Der Parameter, den Sie der Thread-Funktion übergeben wollen.
dwCreationFlags -- 0 bedeutet das der Thread sofort nach seiner Erzeugung zu laufen beginnt. Das Gegenteil ist das CREATE_SUSPENDED Flag.
lpThreadId -- Die CreateThread-Funktion füllt die Thread ID des neu erzeugten Threads an dieser Adresse.

Wenn der CreateThread-Aufruf erfolgreicht war, wird das Handle des neu erzeugten Threads zurückgeliefert, ansonsten NULL.

Die Thread-Funktion läuft sobald der CreateThread-Aufruf erfolgreich abgeschlossen wurde, es sei denn, Sie geben das CREATE_SUSPENDED Flag in dwCreationFlags an. In diesem Fall ist der Thread solange suspendiert, bis die ResumeThread-Funktion aufgerufen wird.

Wenn die Thread-Funktion mit einem ret Befehl zurückkehrt, ruft Windows die ExitThread-Funktion implizit für die Thread-Funktion auf. Sie können die ExitThread-Funktion in ihrer Thread-Funktion selbst aufrufen, aber es gibt kaum Gründe das zu tun.

Sie können den Exit Code eines Threads ermitteln, indem Sie die GetExitCodeThread-Funktion aufrufen.

Wenn Sie einen Thread aus einem anderen Thread terminieren wollen, können Sie die TerminateThread-Funktion aufrufen. Aber Sie sollten diese Funktion unter extremen Umständen benutzen, da die Funktion den Thread sofort terminiert, ohne dem Thread eine Chance zu geben, sich selbst aufzuräumen.

Nun kommen wir zu den Kommunikations-Methoden zwischen den Threads.

Es gibt drei von ihnen:

  • Benutzung von globalen Variablen
  • Windows Nachrichten
  • Ereignisse (Event)
Threads teilen sich die Prozess' Resourcen inklusive der globalen Variablen, somit können Threads globale Variable für die Kommunikation untereinander benutzen. Wie dem auch sei, muss diese Methode mit Vorsicht benutzt werden. Thread-Synchronisation muss in Betracht gezogen werden. Wenn zwei Threads zum Beispiel die selbe Struktur bestehend aus 10 Elementen benutzt, was passiert, wenn Windows auf einmal die Kontrolle von einem Thread zum anderen übergibt, wenn der mitten in der Aktualisierung der Struktur steckte? Der andere Thread wird mit einer Inkonsistenz der Daten innerhalb der Struktur allein gelassen! Machen Sie keine Fehler, multithreading Programme sind viel schwieriger zu debuggen und zu warten. Diese Art von Bugs scheint zufällig aufzutreten, was es sehr schwer macht, sie aufzuspüren.

Sie können auch Windows-Nachrichten für die Kommunikation zwischen zwei Threads benutzen. Wenn die Threads alle Benutzer-Schnittstellen sind, gibt's kein Problem: diese Methoden können als bi-direktionale Kommunikation benutzt werden. Alles was Sie machen müssen, ist eine oder mehr eigene Windows-Nachrichten zu definieren die Bedeutungsvoll für diese Threads sind. Sie definieren eine eigen Nachricht indem Sie die WM_USER-Nachricht als Basis-Wert nehmen, Sie können sie wie folgt definieren:

        WM_MYCUSTOMMSG equ WM_USER+100h


Windows wird keine Werte von WM_USER aufwärts für eigene Werte benutzen, so dass Sie den Wert WM_USER und dadrüber als ihre eigenen Nachrichten-Werte benutzen können. Wenn einer der Threads eine Benutzer-Schnittstelle ist und ein weiterer ein arbeitender, dann können Sie nicht diese Methode der bi-direktionalen Kommunikation benutzen, da der arbeitende Thread kein eigenes Fenster hat und somit auch keine Nachrichten-Warteschlage. Sie können folgendes Schema benutzen:

                            Benutzer-Schnittstellen-Thread ------ globale Variable(en)---- arbeitender Thread
                            arbeitender Thread  ------ eigene Fenster-Nachricht(en) ---- Benuzter-Schnittstellen Thread

In Wirklichkeit benutzen wir diese Methode in unserem Beispiel.

Die letzte Kommunikations-Methode ist ein Event-Objekt. Sie können ein Event-Objekt als eine Art von Flag betrachten. Wenn das Event-Objekt im "nicht-signalisiertem" Zustand ist, ist der Thread nicht aktiv oder schläft, in diesem Status erhält der Thread keine CPU-Zeit. Wenn das Event-Objekt im "signalisiertem" Status ist, "weckt" Windows den Thread "auf" und er startet seine zugewiesenen Aufgaben wieder.

Beispiel:

Sie sollten die Beispiel-Zip-Datei herunterladen und thread1.exe ausführen. Klicken Sie auf das Menü-Element "Savage Calculation". Das veranlasst das Programm die Anweisung "add eax, eax" 600.000.000 mal auszuführen. Beachten Sie, dass Sie während dieser Zeit nichts im Haupt-Fenster machen können: Sie können es nicht bewegen, Sie können die Menüs nicht aktivieren, usw. Wenn die Berechnung beendet wurde, erscheint eine Message Box. Danach akzeptiert das Fenster ihre Eingaben wieder ganz normal.

Um diese Art von Unbequemlichkeit für den Benutzer zu vermeiden, können wir die "Berechnungs" Routine in einem eigenen Thread auslagern und den Haupt-Thread mit seinen Benutzer-Eingaben Aufgaben weiter laufen lassen. Sie können sehen, dass, auch wenn das Haupt-Fenster etwas langsamer antwortet als normal, immer noch reagiert.

.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 "Win32 ASM MultiThreading Example",0 MenuName db "FirstMenu",0 SuccessString db "The calculation is completed!",0 .data? hInstance HINSTANCE ? CommandLine LPSTR ? 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 .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,\ 0,\ ADDR ThreadID invoke CloseHandle,eax .else invoke DestroyWindow,hWnd .endif .endif .ELSEIF uMsg==WM_FINISH invoke MessageBox,NULL,ADDR SuccessString,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 dec ecx jz Get_out jmp Loop1 Get_out: invoke PostMessage,hwnd,WM_FINISH,NULL,NULL ret ThreadProc ENDP end start
 

Analyse:

Das Haupt-Programm präsentiert dem Benutzer ein normales Fenster mit einem Menü. Wenn der Benutzt das Menü-Element "Create Thread" auswählt, erzeugt das Programm einen Thread wie folgt:

.if ax==IDM_CREATE_THREAD mov eax,OFFSET ThreadProc invoke CreateThread,NULL,NULL,eax,\ NULL,0,\ ADDR ThreadID invoke CloseHandle,eax


Die obige Funktion erzeugt einen Thread, der eine Prozedur names ThreadProc neben dem Hauptthread laufen lassen wird. Nach dem erfolgreichen Aufruf, kehrt CreateThread sofort zurück und ThreadProc fängt an zu laufen. Da wir keine Thread-Handle benutzen, sollten wir sie schließen, da ansonsten Speicherlecks entstehen. Beachten Sie, dass das Schließen des Thread-Handles den Thread nicht beendet. Der einzige Effekt ist, dass wir das Thread-Handle nicht mehr benutzen können.

ThreadProc PROC USES ecx Param:DWORD mov ecx,600000000 Loop1: add eax,eax dec ecx jz Get_out jmp Loop1 Get_out: invoke PostMessage,hwnd,WM_FINISH,NULL,NULL ret ThreadProc ENDP


Wie Sie sehen können, führt ThreadProc eine wilde Berechnung aus, die eine Weile dauert, bis sie abgeschlossen ist und wenn sie abgeschlossen ist, eine WM_FINISH-Nachricht an das Haupt-Fenster sendet. WM_FINISH ist unsere eigene Nachricht, die wie folgt definiert ist:

WM_FINISH equ WM_USER+100h


Sie müssen WM_USER nicht 100h hinzu addieren, aber es sicherer es zu tun.

Die WM_FINISH-Nachricht ist nur in unserem Programm von Bedeutung. Wenn das Haupt-Fenster die WM_FINISH Nachricht erhält, antwortet es, indem eine MessageBox angezeigt wird, die mitteilt, dass die Berechnung beendet wurde.

Sie können verschieden Threads in Folge starten, indem Sie "Create Thread" mehrmals auswählen.

In diesem Beispiel ist die Kommunikation nur in eine Richtung gerichtet, so dass nur der Thread dem Haupt-Fenster etwas mitteilen kann. Wenn Sie den Haupt-Thread dazu veranlassen wollen Kommandos an den arbeitenden Thread zu schicken, können Sie folgendes tun:

  • Fügen Sie ein Menü-Element dem Menü hinzu, dass so was wie "Thread killen" sagt
  • eine globale Variable welche als Kommando-Flag benutzt wird. TRUE=Stopp den Thread, FALSE=Thread fortsetzen
  • Bearbeiten Sie ThreadProc damit der Wert des Kommando-Flags in der Schleife überprüft wird.
Wenn der Benutzer das Menü-Element "Thread killen" auswählt, setzt das Haupt-Programm den Wert TRUE im Kommando-Flag. Wenn ThradProc sieht, dass der Wert des Kommand-Flags TRUE ist, verlässt es die Schleife und kehrt zurück, also beendet den Thread.


Deutsche Übersetzung: Joachim Rohde
Die original Win32Asm-Tutorials stammen von Iczelion's Win32 Assembly HomePage