Where Angels Fear to Thread


The Test Thread program shown in Figure 11-6 starts a thread and lives to tell the tale—or at least it does if you can keep your hands off the debugging commands. That’s the first rule of thread debugging in the IDE. Don’t. Debug.Print seems to work for displaying status messages, but that’s about it.



Figure 11-6. The Test Thread program.


The program consists of two buttons that update four text boxes with data maintained by the thread. The main button toggles between starting the thread while passing it a parameter, and stopping the thread while receiving a return value:

Private Sub cmdStartStop_Click()
If cmdStartStop.Caption = "&Start Thread" Then
StartThread txtStartStop
cmdStartStop.Caption = "&Stop Thread"
lblStartStop.Caption = "Return value:"
cmdUpdate_Click
Else
txtStartStop = StopThread
cmdStartStop.Caption = "&Start Thread"
lblStartStop.Caption = "Argument:"
cmdUpdate_Click
End If
End Sub

Private Sub cmdUpdate_Click()
txtCalc = CalcCount
txtAPI = APICount
txtBasic = BasicTime
End Sub

The CalcCount, APICount, and BasicTime functions are just public functions that return private variables modified by the thread procedure in THREAD.BAS. The thread procedure, like any procedure used with the AddressOf operator, must be in a standard module. You’re used to that from earlier experience with callbacks in Chapters 2 and 6. Here’s the code to start the thread procedure:

Sub StartThread(ByVal i As Long)
' Signal that thread is starting
fRunning = True
' Create new thread
hThread = CreateThread(ByVal pNull, 0, AddressOf ThreadProc, _
ByVal i, 0, idThread)
If hThread = 0 Then MsgBox "Can't start thread"
End Sub

First we signal that the thread is starting by using the most primitive and inflexible method of thread synchronization—a global variable. Bear with me. Then we call CreateThread to start a thread in the procedure ThreadProc. The first argument is the security attribute, which we ignore. The second is the stack size, which we pass as zero to let the operating system decide. The third argument is the address of the procedure that will run the thread. The fourth is a 32-bit parameter passed to the thread. This could be a pointer to something useful (such as a string or a UDT), but in this case, it’s just numeric data. The fifth argument is for options flags, but we don’t have any. The last argument is a reference to a variable that will receive the thread ID, a unique identifier which the sample program stores in the global variable idThread and then ignores. The function returns a thread handle, which is also stored in a global variable.


The thread procedures you start have some leeway in what they do, but some parts are invariant, as this example shows:

Sub ThreadProc(ByVal i As Long)
' Use parameter
cCalc = i
Do While fRunning
' Calculate something
cCalc = cCalc + 1
' Use an API call
cAPI = GetTickCount
' Use a Basic function
datBasic = Now
' Switch immediately to another thread
Sleep 1
Loop
' Return a value
ExitThread cCalc
End Sub

The main purpose of this thread is to prove that we can indeed run a thread and do calculations, make API calls, and use Visual Basic statements inside it. After updating several global variables, the thread sleeps so that other threads can continue working immediately. When the main thread (the calling process) signals to stop by changing the fRunning variable, the thread exits by calling ExitThread, passing whatever it wishes to return to the process that started it.


The documentation for CreateThread says that the thread procedure can be a function that returns a Long value and that ExitThread will be called automatically. Well, that might be true in C or C++, but I crashed when I made ThreadProc a function and returned without explicitly calling ExitThread. I changed to a sub to make absolutely clear that the return value must go through ExitThread.


My StopThread procedure is what causes the thread loop to terminate so that ExitThread will be called:

Function StopThread() As Long
' Signal thread to stop
fRunning = False
' Make sure thread is dead before returning exit code
Do
Call GetExitCodeThread(hThread, StopThread)
Loop While StopThread = STILL_ACTIVE
CloseHandle hThread
hThread = 0
End Function

It might take a while after the global variable is changed before the thread procedure checks the signal variable and terminates the thread, so StopThread has to loop until GetExitCodeThread comes up with a valid exit code.