1. Nachteil:
Man Betrachte folgenden Code-Auschnitt:
Das Timer-Ereigniss wird laut Dokumentation ausgelöst, wenn ein Zeitintervall, welches in Millisekunden angegeben wird, durchlaufen ist.Private Sub Timer1_Timer().End Sub
. (Code welcher abgearbeitet wird)
.
2. Nachteil:
Das die Inervalllänge des Timer-Ereignisses läßt sich
zwar bis auf eine Millisekunde einstellen, jedoch gibt es eine Plattformabhängige
untere Schranke weit oberhalb einer Millisekunde. Unter Win95 ist diese
Schranke 50 ms und unter WinNT4 10 ms. Das Timer-Ereigniss erreicht also
unter WinNT eine maximale Frequenz von 100 Hz.
Die Funktion wird wie folgt deklariert:
Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As LongUm die Frequenz des Counters zu erfahren enthält das Windows-API die Funktion QueryPerformanceFrequency, die ebenfalls eine LARGE_INTEGER-Variable, in Visual Basic also eine Variable vom Typ Currency, mi der Frequenz des Counters belegt.
Die Funktion wird wie folgt deklariert:
Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As LongDie vergangene Zeit zwischen zwei Aufrufen von QueryPerformancCounter berechnet sich dann aus der Differenz des Zählerstandes geteilt durch seine Frequenz.
Dim Frequency as CurrencyObwohl die Frequenz des Counters sehr hoch ist gibt es auch hier eine untere Schranke, da beide Funktionen ca 5 µs benötigen um den Port auszulesen.
Dim Count0 as Currency
Dim Count1 as Currency
Dim HowLong as Double
Dim ErcL as LongErcL = QueryPerformanceFrequency Frequency
If ErcL = 0 then End ' Ther is no high-performance-counter in the systemQueryPerformanceCounter Count0
.
. (code)
.
QueryPerformanceCounter Count1HowLong = Cdbl((Count1-Count0) / Frequency) 'Elapsed time in seconds
Das Thema "Multithreading" stellt für den VisualBasic-Programmierer ein heikles Thema dar bei dem viele Probleme auftauchen. Viele neue Begriffe stürzen auf ihn ein und es würde ein ganzes Buch füllen es ausreichend darzulegen. Dem Leser sei es überlassen sich darin an anderer Stelle einzuarbeiten da auch mir dieses Thema nur unzureichend geläufig ist und ich keine Quelle kenne die das Thema eingehend für Visual Basic beleuchtet. Eine kleine Einführung findet man in der c't 98.
Für dieses Problem müssen wir jedoch nur einen Thread,
indem die Timer-Schleife arbeitet, starten und ihn anschließen wieder
löschen. Als Synchronisationselement bietet sich hier ein Event an,
welches durch den Parentprozess signalisiert und vom Childthread auf dessen
Zustand überprüft wird. Ist das Event signalisiert, so wird die
Schleife mit einem Exit Do verlassen und der Childthread beendet
sich selbst.
Gestartet wird der Thread mit der Funktion CreateThread, welche
wie folgt deklariert wird:
Declare Function CreateThread Lib "kernel32" (lpThreadAttributes As SECURITY_ATTRIBUTES, ByVal dwStackSize As Long, _Dem Argument LpTreadAttrbutes wird eine unbelegte Struktur übergeben, da der Handle auf den Thread nicht durch Childprozesse geerbt werden brauch, dwStacksize wird 0& übergeben damit Windows selbst die Stackgröße verwaltet, lpStartAddress wird die Adresse der Scheifenprozedur übergeben, lpParameter ist ein Argument welches der Schleifenprozedur übergeben wird, dwCreationFlags wird CREATE_SUSPENDED übergeben damit der Thread noch nicht ausgeführt wird und lpThreadId wird von der Funktion mit der Thread-ID belegt.
ByVal lpStartAddress As Long, lpParameter As Long, ByVal dwCreationFlags As Long, _
lpThreadId As Long) As Long
Declare Function SetThreadPriority Lib "kernel32" (ByVal hThread As Long, ByVal nPriority As Long) As LongHier wird hThread der Handle auf den Thread und nPriority eine der folgenden Konstanten übergeben.
Konstanten für nPriority : THREAD_PRIORITY_IDLEDie Benutzung von THREAD_PRIORITY_TIME_CRITICAL ist wenn möglich zu vermeiden, da kein anderer Thread Prozessorzeit bekommt.
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_TIME_CRITICAL
Private Declare Function ResumeThread Lib "kernel32" (ByVal hThread As Long) As LongDie Schleifenfunktion läuft nun als unabhängiger Thread quasiparallel zum Parentprozess und wird vom Scheduler mit Prozessorzeit versorgt.
Hierzu habe ich als Synchronisationsobjekt ein Event gewählt. Ein
Event ist ein Objekt welches Systemweit gültig ist und zwei Zustände
, signalisiert und nicht signalisiert, kennt.
Nachdem ein Event mit der Funktion CreateEvent erschaffen wurde,
soll es Systemweit als unsignalisiert mit seinen Handle zu "sehen"
sein, also auch in unserem unabhängigem Thread. Die Schleifenfunktion
soll nun bei jedem Schleifendurchlauf den Zustand dieses Events abfragen
und solange es unsignalisiert ist mit der Schleife fortfahren. Ist es jedoch
signalisiert, so soll die Schleife verlassen werden, der Zustand des Events
wieder unsignalisiert und der Thread verlassen werden.
Wie schon erwähnt wird dieses Event durch die Funktion CreateThread
erzeugt, welche einen Handle auf das Event liefert.
Declare Function CreateEvent Lib "kernel32" Alias "CreateEventA" (lpEventAttributes As SECURITY_ATTRIBUTES, _Dem Argument lpEventAttrbutes wird eine unbelegte Struktur übergeben, da der Handle auf das Event nicht durch Childprozesse geerbt werden brauch, bManualReset wird 1& übergeben damit es explizit zurückgesetzt werden muß, bInitialState wird 0& übergeben um das Event unsignalisiert zu erschaffen und lpName wird vbNullString übergeben, da der Name uns nicht interessiert.
ByVal bManualReset As Long, ByVal bInitialState As Long, _
ByVal lpName As String) As Long
Declare Function SetEvent Lib "kernel32" (ByVal hEvent As Long) As LongDie Schleife im unabhängigen Thread fragt nun in jedem Schleifendurchlauf mit der Function WaitForSingleObject den Zustand des Events ab.
Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As LongHierbei ist hHandle der Handle auf das Event und dwMilliseconds wird zu 0& gesetzt damit die Funktion ohne Wartepause zurückgibt. Ist der Rückgabewert gleich WAIT_OBJECT_0, dann ist das Event signalisiert und die Schleife wird mit einem Exit Do verlassen. Die Konstante WAIT_OBJECT_0 hat den Wert &H0 und ist im API-Viewer von Visual Basic nicht zu finden.
Declare Function ResetEvent Lib "kernel32" (ByVal hEvent As Long) As LongDem Argument hEvent wird der Eventhandle übergeben und dwExitCode ein Long-Wert der uns aber nicht weiter interessiert.
Declare Sub ExitThread Lib "kernel32" (ByVal dwExitCode As Long)
Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" (lpMutexAttributes As SECURITY_ATTRIBUTES, _Auch hier wird lpMutexAttributes ein eine unbelegte Struktur übergeben, da der Mutexhandle nicht von Childprozessen geerbt werden brauch. Dem Argument bInitialOwner wird 0& übergeben, da der Mutex keinen voreingestellten "Besitzer" haben soll und lpName wird vbNullString übergeben, da uns sein Name nicht interessiert. Rückgabewert von CreateMutex ist der Handle hHandle auf den Mutex.
ByVal bInitialOwner As Long, ByVal lpName As String) As Long
Declare Function ReleaseMutex Lib "kernel32" (ByVal hMutex As Long) As Long
Mit dem hier dargestellten Weg lassen sich auch sehr gut sogenannte
Working-Threads implementieren, wie das Einlesen von Daten und das Drucken.
Er stellt jedoch keine allgemeingültige Möglichkeit des Multithreadings
unter Visual Basic dar, da nicht alle Objekte und Funktionen von Visual
Basic threadsicher sind.
Zudem ist der Weg nicht mit dem OLE-Threading-Modell kompatibel, so
das hier Abstürze vorprogrammiert sind. Wenn möglich sollte man
auf API-Funktionen ausweichen, da diese auf jeden Fall threadsicher sind.
Auf das Testen und Debuggen in der VB-IDE muß man ganz verzichten
da diese nur den Hauptthread unterstützt und fast immer beim Aufruf
von CreateThread zusammenbricht. Man implementiert also erst die
Working-Threads ohne separatem Thread und wenn alles läuft so implementiert
man den unabhängigen Thread.
Gelobt sei wer nun einen Debugger besitzt welcher mehrere Threads debuggen
kann wie z.B. der Debugger in Visual C++ 5.
Man darf auf keinen Fall vergessen, das der wichtigste Aspekt beim
Multithreading die Synchronisation der einzelnen Threads ist. Kleine Denkfehler
in der Planung sorgen hier unweigerlich zum Crash. Mit einem 200 MHz AMD-Prozessor
lief eine ältere Version dieses Timers ohne Probleme, auf einem 266
MHz AMD-Prozessor gab es immer einem GPF. Dabei hatte ich den Mutex nur
an einer falschen Steller erschaffen. So hatte zwar jede Instanz einen
eigenen Mutex jedoch keinen gemeinsamen mehr.
Ich hoffe das andere Leute diesen Code verwenden oder erweitern können
und würde mich über ein Feedback freuen.
Hier nun die Module:
Download der Projektdateien
(C) 1998 by Karsten Schneider