Multi-Threading for Basic Programmers
Part 3: May the "Power" be with You
By Jason Bock
Click here to download the source code for this article!
Introduction
In this article, I'm going to show you how you can use PowerBASIC to spawn
multiple threads, and how they can be controlled in Visual Basic. My focus will
be on demonstrating the core concepts of multithreading rather than focusing on
how one would use threads to access databases or download web pages. Many books
and articles have been written on multithreading in applications, and I don't
want to repeat their work. Hopefully, by the end of this article, you'll be
able to take your new-found knowledge of threads and apply it to your specific
business needs by using PowerBASIC to handle your threads in Visual Basic
applications.
VB6 and Threading: Why PowerBASIC is Needed
If you're a VB developer who has delved into the mysteries of the
CreateThread API call, you've noticed that it works in version 5.
Granted, it takes some time to get used to the complexities of multithreading as
well as making sure the API declaration is correct. But once the initial pain
of understanding how threads work and communicate with each other is over,
multithreading in VB was achievable. You've probably also noticed that, as hard
as it is to control the threads, the power gained by using threads in applicable
situations is amazing. For example, I've created an NT service in VB5 that
would track usage statistics of every database within SQL Server. This could
have been done using separate processes, but by having different threads handle
specific, isolated tasks, I was able to wrap it all up into one process. This
made testing harder, but the end result was easy to install and distribute.
I should note that all versions of VB as of the writing of this article are
not thread-safe. If you do use CreateThread in VB5, you're going
to have to avoid anything that VB has to offer in the separate threads. Don't
try to access a form or any controls on the form, don't use the App
object's properties, etc. If you do, you run the risk of a memory
exception.
How things have changed with VB6! If you've used CreateThread
in VB before and tried to run those projects as a compiled EXE under VB6, you
probably received a horrific message from the operating system telling you that
an access violation has occurred. Microsoft has changed the inner workings of
VB such that spawning threads via Win32 API calls is a big no-no.
Why this changed and what was done is still unclear at the
moment, but Microsoft has recently posted an article on their web site (which
you can view by clicking here)
that acknowledges the internal change.
Now for most VB programs, you'll probably never need to spawn threads to
handle what a user wants. But there are definitely situations (like printing
reports) where a separate thread could be very useful. Granted, you could
develop an apartment-threaded component that would run each object on a
different thread, but since VB is by nature single-threaded, you don't gain a
lot of threading power by using this approach.
However, PowerBASIC allows developers to spawn threads to their hearts'
content by using the keyword Thread along with other keywords like
Create, Suspend, Resume, and
Close. Also, PowerBASIC allows you to export your procedures so
other Windows programs can use your code. Therefore, we can use the
multithreading capabilites of PowerBASIC to create threads in VB6. There's a
small "hoop" that we have to dive through in order to communicate between the
threads in your PowerBASIC DLL to the VB application. But once we're through
it, we can use knowledge over and over in future systems. Therefore, let's take
the time to see just how we can combine the two tools to enable a VB program to
spawn threads.
The Main Design Issue: Communicating With VB Using a Callback
Before we get into the code, let's step back and think about how VB and
PowerBASIC will talk to each other. The easiest and most feasible way would be
to use a callback function. VB introduced the AddressOf keyword in
version 5 to allow VB developers access to Win32 API calls that required a
function to call back on. However, there's a slight snag: VB doesn't like it if
the function is called from any other thread other than the main VB thread
(click here
for further information from Microsoft on this issue). Therefore, we can only
call back on the thread that told us what function to call.
How do we accomplish this? If you're comfortable with Windows development
apart from VB, you know that one easy way to do this is to create a window on
the calling thread. We can then set up our own user-defined window message that
any thread can use as a "router" to perform the callback. Since the window
lives on the same thread as the calling thread, no memory exceptions will occur,
which is always nice to avoid.
Now if you've never really worked with the Win32 API calls, the previous
paragraph may have sounded like gibberish. That's OK - don't worry about
sweating the details right now. Just remember that our goal is to use a window
as our router to make the communication between the PowerBASIC DLL and the VB
EXE very smooth. And (shameless plug begins) I've also written a book about
Win32 programming in VB that may help the VB developer who hasn't work with
Win32 API calls before - just look in the References section at
the end of this article for further details (end very shameless plug).
PBTHREAD.DLL Implementation
So what do we want our DLL to do? Let's keep it very simple for the purposes
of illustration. We'll export one function that will allow a VB program to
determine how many threads should be created. We'll assume that our "work" is
that the threaded function sleeps for a small amount of time (quite the cushy
job, isn't it?). Also, the function should have a parameter that takes a
function address such that the DLL can notify the calling application when the
work is done.
Sounds simple enough, right? Well, let's get started by adding some startup
code for our DLL:
$COMPILE DLL "PBThread.DLL"
$INCLUDE "WIN32API.INC"
Declare Function SpawnThreads (ByVal NumberOfThreads As Long, _
ByVal CallbackFunctionAddress As Long) As Long
Function LibMain(ByVal hInstance As Long, ByVal fwdReason As Long, ByVal lpvReserved As Long) Export As Long
Select Case fwdReason
Case %DLL_PROCESS_ATTACH
LibMain = 1
Exit Function
Case %DLL_PROCESS_DETACH
LibMain = 1
Exit Function
Case %DLL_THREAD_ATTACH
LibMain = 1
Exit Function
Case %DLL_THREAD_DETACH
LibMain = 1
Exit Function
End Select
End Function
Most of this code is just boilerplate to get us started. Rest assured, a lot
more will be added. As you can see, there's a function declaration for
SpawnThreads, so let's take a look at how that function works.
For space considerations, I haven't included any comments in the code examples, but it is in the source (which you can download using the link at the beginning of this article). You'll also notice our SpawnThreads function, which is declared at the top of the PBThread.BAS file. We'll add more procedures, global variables, and other code goodies as we move along, so if things get a bit confusing, please download the source for reference purposes. I'll try to note when I've added code to areas we've already covered, but keep the source code close - it'll make the explanations easier.
Implementing SpawnThreads
From our initial discussion, coding the SpawnThreads function
sounds simple enough - create a bunch of threads and let the calling application
know when the work's done. Let's take a look at the code:
Function SpawnThreads Alias "SpawnThreads" (ByVal NumberOfThreads As Long, _
ByVal CallbackFunctionAddress As DWord) Export As Long
On Error Goto Error_SpawnThreads
Dim ascWindowName As Asciiz * 80
Dim lngC As Long
Dim lngHeap As Long
Dim lngHeapPtr As Long
Dim lngHThreadResult As Long
Dim lngHWnd As Long
Dim lngIndex As Long
Dim lngLength As Long
Dim lngRet As Long
Dim lngThreadID As Long
Dim udtThreadInfo As ThreadPostInfo
Call WaitForSingleObject(glngGeneralMutex, %INFINITE)
Function = %FALSE
lngThreadID = GetCurrentThreadID()
ascWindowName = "MTExample" & Str$(lngThreadID)
lngHWnd = FindWindow(ByVal %NULL, ascWindowName)
If lngHWnd = %NULL Then
lngHWnd = CreateCallbackWindow(lngThreadID)
End If
If lngHWnd <> 0 Then
lngLength = SizeOf(udtThreadInfo)
lngHeap = GetProcessHeap
If lngHeap <> %NULL Then
For lngC = 1 To NumberOfThreads
lngHeapPtr = HeapAlloc(lngHeap, 0, lngLength)
If lngHeapPtr <> 0 Then
udtThreadInfo.WindowHandle = lngHWnd
udtThreadInfo.CallbackAddress = CallbackFunctionAddress
MoveMemory lngHeapPtr, VarPtr(udtThreadInfo), lngLength
Thread Create SleepForAWhile(lngHeapPtr) To lngHThreadResult
Thread Close lngHThreadResult To lngRet
End If
Next lngC
Function = %TRUE
End If
End If
Call ReleaseMutex(glngGeneralMutex)
Exit Function
Error_SpawnThreads:
Call ReleaseMutex(glngGeneralMutex)
End Function
There's a lot of code here, so let's take it step by step. The first thing
we do is wait on a mutex. A mutex is a Win32 kernel object
that allows a Windows developer to "lock" a piece of code such that one and only
one thread can execute that code. You use the CreateMutex API call
to create it, and the WaitForSingleObject API call to...well,
"wait" for it. If no other thread currently owns the mutex,
WaitForSingleObject will return, and now you have ownership of the
mutex. Of course, you have to release it so you don't block a lot of other
threads for a long time, which is what the ReleaseMutex API call is
for. The reason we do this here is that, even though we know VB is
single-threaded, we'd like to let other Windows programs written in other
languages use our DLL. We want to make sure that we spawn a set of threads as
one atomic action, and since there's no guarantee that other Windows programs
will be single-threaded, we use a mutex to guarantee this. Note that the
glngGeneralMutex is declared as a Global variable, and
is initialized when %DLL_PROCESS_ATTACH is received in
LibMain.
OK, now that we have the mutex, we search to see if a window exists with the
title "MTExample" concatenated with the current thread. If one doesn't exist,
we create one using the CreateCallbackWindow function. The first
part's not too hard to understand. If we've already created our routing window
for the calling thread, there's no reason to make another and waste resources.
Since we're controlling the window creation, we embed the thread ID into the
window name, so it makes finding the window a pretty painless job. This is
accomplished by using the FindWindow API call. But what's
CreateCallbackWindow? Well, that will be handled in a section to
come called "YADLL" for "Yet Another DLL." Yes, we're going to make another
DLL, but we'll worry about that later. For now, assume that this function will
create the necessary routing window we need for successful communication.
In anticipation of this other DLL, the following function declaration should be added to our PBThread.BAS file:
Declare Function CreateCallbackWindow Lib "PBThrdWd.Dll" Alias "CreateCallbackWindow" (ThreadID As Long) As Long
So now that we have a valid window handle, our threads will know what window
to communicate with. But we have one more piece of code before we can create
the threads. If you look at the Thread Create syntax, we're
allowed to pass one and only one argument to the threaded function. That
doesn't leave a lot of options available to communicate any information to the
threaded function. However, there's a simple way around this! If we use the
process's heap memory, we can store an entire UDT into a specific block of
memory (which is done using the GetProcessHeap,
HeapAlloc, and MoveMemory API functions) , and pass
the memory address to the threaded function. When the thread starts up, it can
read that memory, copy it into its' own UDT, and deallocate the memory. As you
can see, this DLL uses a UDT called ThreadPostInfo, which is
declared as follows:
Type ThreadPostInfo
WindowHandle As Long
CallbackAddress As DWord
End Type
Therefore, we can let the threaded function know not only the window that it
needs to communicate with, but the callback function address that our client
wants us to callback on. This lets us be pretty flexible, as our client may
have different callback functions for different processes.
One technical point should be made here about using process heap memory for "extending" the threaded function's parameters. You may just say, "well, change the UDT as needed, and pass the address of the UDT to the threaded function." This doesn't work. The threaded function will get an address to a UDT in memory, but if we did that in our DLL, the thread wouldn't necessarily get the information we thought it should. For example, in our DLL the UDT's information gets reset in the For...Next loop. Granted, that information doesn't change, but it may in other DLL implementations, and if each thread gets the same pointer to the UDT, they're all looking at the same UDT! Who knows what each thread will see when they access that UDT! The only way around this is to create as many UDTs as there are threads, and then each thread would get the correct information. But also remember, when the function that creates the threads leaves the stack, so does the information in the UDT. That's why I recommend storing the information somewhere where it won't disappear easily.
Finally, we're going to create threads! As you can see, it's really easy in
PowerBASIC - the Thread Create statement does it all. We pass our
address to the information in the heap as the argument to
SleepForAWhile, and we get the thread's handle in
lngHThreadResult. We close that handle in the next line of code
using the Thread Close statement. Note that doing this does
not kill the thread. It simply closes the handle to the
thread. When the thread is finished, it will then be removed from the stack.
If we didn't close the handle, the thread would still stick around until the
entire process shuts down. Therefore, we close the handle right after thread
creation. You don't have to do this in every situation; in fact, some problems
require you to store thread handles (like a thread scheduler, for example). But
for our purposes, we'll simply close the handle.
Implementing SleepForAWhile
Compared to what we saw in SpawnThreads, this code is pretty
painless:
Function SleepForAWhile(ByVal HeapPtr As Long) As Long
On Error Goto Error_SleepForAWhile
Dim lngLength As Long
Dim lngProcessHeap As Long
Dim lngRet As Long
Dim lngSleepTime As Long
Dim udtThreadInfo As ThreadPostInfo
lngLength = SizeOf(udtThreadInfo)
MoveMemory VarPtr(udtThreadInfo), HeapPtr, lngLength
lngProcessHeap = GetProcessHeap
lngRet = HeapFree(lngProcessHeap, 0, HeapPtr)
lngSleepTime = Rnd(%MIN_SLEEP_TIME, %MAX_SLEEP_TIME)
Call Sleep(lngSleepTime)
Call SendMessage(udtThreadInfo.WindowHandle, %MT_NOTIFY, udtThreadInfo.CallbackAddress, %TRUE)
Exit Function
Error_SleepForAWhile:
Call SendMessage(udtThreadInfo.WindowHandle, %MT_NOTIFY, udtThreadInfo.CallbackAddress, %FALSE)
End Function
The first thing we do is get the information out of the process heap memory,
store it in udtThreadInfo, and deallocate the heap memory. Then,
we execute some really hard and torturous code: We sleep for a
while. OK, that's not too exciting, but it simulates background work, and it
also brings up a good point about multithreading. One big misconception about
adding threads to an application is that it will immediately speed up your code.
For some problems, multithreading fits the bill. For others, adding threads to
break up tasks will actually hurt program performance, especially if you only
have one processor. Why? Because the OS has to perform what's known as a
context switch when more than 1 thread exists for the OS to
handle. This context switch isn't free - it takes some time for the OS to store
what your thread was doing and let another one run - and if your threads take
the same amount of time to perform the work, you'll actually see a degradation
due to this context switching. However, since we're going to sleep anywhere
from 1 millisecond to 5 seconds (the values for the %MIN_SLEEP_TIME
and %MAX_SLEEP_TIME constants, respectively), our work time is
pretty disparate and, in our case, multithreading sleep time of varying lengths
isn't a bad thing to do.
Well, once our lazy thread wakes up, it actually does something very
critical. It uses the SendMessage API call to let the routing
window know (through the first argument) that we're done with our work (through
the second argument set to our user-defined window message). We also let the
window know what the callback function address is (that's the third argument's
value) and if we ran into any errors or not (the fourth argument). It's a very
simple call, but if it's not made, our calling application would have no way of
knowing that our thread is done.
But how does our routing window know what to do with the message? And how is
it created in the first place? Read on...
YADLL: PBTHRDWD.DLL Implementation
As I mentioned earlier when we ran into the CreateCallbackWindow
function in SpawnThreads, we have another DLL to create. We're
simply separating the threading code from the routing windows code; although
that leaves us with 2 DLLs, we can focus on the problems apart from each other.
As you'll see in a moment, there are only two functions in this DLL that we have
to worry about. Let's start with the exported function
CreateCallbackWindow.
Note that the code for this DLL is in the file called PbThrdWd.bas.
Implementing CreateCallbackWindow
This function is actually very straighforward. We register the window class
using RegisterClassEx, and then we create the window via
CreateWindowEx. Here's the code:
Function CreateCallbackWindow Alias "CreateCallbackWindow" (ThreadID As Long) Export As Long
On Error Resume Next
Dim ascClassName As Asciiz * 80
Dim ascWindowName As Asciiz * 80
Dim lngHWnd As Long
Dim lngRet As Long
Dim udtWindowClass As WndClassEx
Function = %FALSE
ascClassName = "MTExample"
udtWindowClass.cbSize = SizeOf(udtWindowClass)
udtWindowClass.style = %NULL
udtWindowClass.lpfnWndProc = CodePtr(WindowProc)
udtWindowClass.cbClsExtra = %NULL
udtWindowClass.cbWndExtra = %NULL
udtWindowClass.hInstance = glnghWDInstance
udtWindowClass.hIcon = %NULL
udtWindowClass.hCursor = %NULL
udtWindowClass.hbrBackground = %NULL
udtWindowClass.lpszMenuName = %NULL
udtWindowClass.lpszClassName = VarPtr(ascClassName)
udtWindowClass.hIconSm = %NULL
lngRet = RegisterClassEx(udtWindowClass)
ascWindowName = "MTExample" & Str$(ThreadID)
lngHWnd = CreateWindowEx(%NULL, _
ascClassName, _
ascWindowName, _
%NULL, _
%CW_USEDEFAULT, _
%CW_USEDEFAULT, _
%CW_USEDEFAULT, _
%CW_USEDEFAULT, _
%HWND_DESKTOP, _
0, _
glngHWDInstance, _
BYVAL %NULL)
Function = lngHWnd
End Function
Again, we're using the string "MTExample" along with the thread
ID as the window name, which allows us to determine via the
FindWindow API call if a routing window has been set up for a given
thread. Now that the window's created, let's see how that window will callback
to the client.
Implementing WindowProc
This function is always called when our routing window receives any kind of
message. Since we set lpfnWndProc equal to
WindowProc, we've subclassed the window, so we can intercept all
the messages. However, as you'll see in the code, we're only concered about two
messages: %WM_CLOSE and %MT_NOTIFY:
Function WindowProc (ByVal hWnd As DWord, ByVal wMsg As DWord, ByVal wParam As DWord, ByVal lParam As Long) As Long
On Error Resume Next
Select Case wMsg
Case %MT_NOTIFY
Call DWord wParam Using CallbackComplete(lParam)
Case %WM_CLOSE
DestroyWindow hWnd
End Select
Function = DefWindowProc(hWnd, wMsg, wParam, lParam)
End Function
The %WM_CLOSE message is pretty easy - we've been notified to
destory the window, which is accomplished by using the
DestroyWindow API function. However, we have to define how to
handle %MT_NOTIFY. We know that we have to perform a callback, and
we know that the function address is contained in wMsg. Therefore,
we can use the Call DWord keywords in PowerBASIC to call the
function with its' address. (Note to VB programmers: If you really like
AddressOf, wait until you get into PowerBASIC. You can actually
use the function address to call a function within your own
code!). CallbackComplete is a function prototype we need to set up
in our DLL such that we can make the call back to the client correctly. We're
assuming that the client has received the proper documentation, and knows that
the callback signature needs to look like this:
Sub CallbackComplete(ByVal Success As Long)
So once our window gets a notification message from a thread, it makes the
appropriate callback and lets the client know if the thread was successful or
not (via the value of lParam). Guess what - we're done. Through 4
functions in 2 DLLs, we can have a multithreaded client in VB without the nasty
crashes.
Of course, we should test this out in VB, right?
Testing Our DLLs in VB
This is the really simple part. All you need to do in VB is create a new
standard EXE project with one form and one module. The form should have a text
box that will be used to determine how many threads to create, and a command
button to start the threads. A list box should be included as well to monitor
the status of the threads. Here's a screen shot of the form:

Our module only needs two things: a Declare for our
SpawnThreads function, and a callback function similar to
CallbackComplete defined above. Let's go through the VB project
from thread creation to thread completion.
Starting the Threads
When the command button cmdStart is clicked, the following code
is run:
Private Sub cmdStart_Click()
StartThreads
End Sub
This may seem a bit strange - why don't I have the code right in the
Click event of the command button? Personally, I don't find that
to be good programming practice. For some events (like the
Initialize event of an object) it makes perfect sense to have code
specific to that event. But in this case, we shouldn't tie thread generation to
a specific button. By separating it out into a different function, we can now
call it from a Click event of a menu item if we so choose.
In any event (no pun intended), let's take a look at what
StartThreads does:
Private Sub StartThreads()
On Error GoTo Error_StartThreads
Dim lngRet As Long
lstRes.AddItem "Starting Time: " & Format$(Now, "mm/dd/yyyy hh:nn:ss")
lngRet = SpawnThreads(CLng(txtThreadCount.Text), AddressOf CallbackComplete)
Exit Sub
Error_StartThreads:
lstRes.AddItem "Error - " & Err.Description
End Sub
All we do is call our DLL function SpawnThreads, using the value
of the text box txtThreadCount to determine how many threads we
want to create, and setting the function address equal to the address of
CallbackComplete. We also add an entry into our list box
lstRes when we called this function. Now that we can generate the
threads, we need to code our callback function.
Receiving Thread Completion Notifications
It really doesn't get any easier than this:
Public Sub CallbackComplete(ByVal Success As Long)
On Error Resume Next
frmPBThreadTest.lstRes.AddItem "Thread Returned, Status Is " & CBool(Success) & ". Time: " & Format$(Now, "mm/dd/yyyy hh:nn:ss")
End Sub
We reference the form's list box, and add an item every time we receive a
callback. Note that this function must exist in a module; VB
does not let you get the address of a function in a form or class module. If
everything is set up correctly, you should see a screen like this:

I should stress that you always run this code as a compiled EXE. VB's IDE is single-threaded, and it really doesn't like it when other threads are running all over the place in a VB project. However, if you run it as an EXE, you should have no problems.
Just to reassure you, I tested this out in VB5 and VB6. In both cases, the
compiled EXE ran just fine.
Conclusions
In this article, we saw how we could use PowerBASIC's capabilities to add
threading support to VB. Hopefully, with this general framework, you'll be able
to add multithreading to your VB applications (or any Windows application for
that matter) using PowerBASIC. There's a lot more that can be done with threads
- I would look at the books in the References section for
further information.
I should point out one final thing...even though these articles followed the
trilogy setup (Part 1, 2, and 3), maybe a 4th article on extending the ideas
presented in this article to real-world situations may not be a bad idea. How
about downloading a bunch of web pages in different threads? Would it work?
What about MSMQ? What can we do there? ODBC? Multiple threads for multiple
database tasks? If you're looking for more, let PowerBASIC or me know, and
maybe a 4th article will surface somewhere in a distant galaxy, far, far away...
I realize that I may have glossed over some topics and code issues in this
article, or that you may have some questions about PowerBASIC and
multithreading, or you might have found some bugs in my examples (perish the
thought!). Please contact PowerBASIC or me (via the e-mail link at the top of
the article), and I'll try to answer your questions as soon as I can. I can't
answer every question I receive, nor can I debug your PowerBASIC and/or VB
projects, but I'll help you out as best I can.
References
Beveridge, Jim; Weiner, Robert, "Multithreading Applications in Win32: The Complete Guide to Threads," Addison-Wesley Pub. Co., 1996.
Cohen, Aaron; Woodring, Mike, "Win32 Multithreading Programming," O'Reilly & Associates, 1998.
Bock, Jason, "Visual Basic 6 Win32 API Tutorial," Wrox Press, 1998.
Acknowledgements
Thanks goes out to the following:
The PowerBASIC staff for letting me write Part 3 of this series.
Dexter Jones, who was brave enough to test out my code on another machine and proofed this article.
Pete Moehrke, who proofed this article.
James C. Fuller, who created the ADPDebugPrint
utility for PowerBASIC programmers. It made my development time a heck of a lot
easier.
About the Author
Jason Bock has received both a Bachelors and Masters Degree in Electrical
Engineering from Marquette University. He has worked primarily in VB since
version 3.0 writing client/server applications for a variety of business
applications, ranging from application tracking systems to payroll processing to
custom query analysis tools. These systems used and/or integrated with a
multitude of different technologies and software packages, such as SQL Server,
COM, Sybase, Oracle, PeopleSoft amd MS Office. He is also the author of Visual
Basic 6 Win32 API Tutorial, published by Wrox Press. Currently, Jason is a
consultant for Keane Inc.
When he's not staring at a computer monitor, Jason enjoys golfing, playing
tennis, weightlifting and biking, spending a lot of time with his wife and
playing with his cat Simon.
Earlier Articles
Did you miss Part 1? Read it here:
Part 1: Use the Force, Luke.
Did you miss Part 2? Read it here:
Part 2: You are not a Jedi yet.
Star Wars ® is a registered trademark of Lucasfilm Ltd.
|