Threads 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 BYVAL lngHeapPtr, BYVAL 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 BYVAL VarPtr(udtThreadInfo), BYVAL 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.