Ingenious Ways to Implement Multiple Threads in
Visual Basic 5.0, Part I
|Visual Basic 5.0 introduces the AddressOf operator, which allows programmers full access to the Win32 APIpointers and all. As long as you follow the guidelines discussed here, you can pass pointers of Visual Basic functions to the CreateThread API and get a true multithreaded app.|
This article assumes you're familiar with Visual Basic and Win32
Code for this article: Multithreading0897.exe (43KB)
John Robbins is a software engineer at NuMega Technologies, Inc., who specializes in debuggers. He can be reached at email@example.com.|
Visual Basic® 5.0 brought with it a slew of new features and capabilities. One feature really stands out and moves Visual Basic into the big leagues of software development: the AddressOf operator. With Visual Basic 5.0 and AddressOf, you can pass pointers to functionscallbacks in the Win32® vernacularto APIs that were off-limits to previous versions. Developers using Visual Basic 5.0 can apply the entire Win32 API to their advantage. I don't know about you, but when I saw that Visual Basic could handle callbacks, I immediately wondered if real multithreading was possible. After all, a callback is the same thing that gets passed
to the lpStartAddress parameter of CreateThreadand
a callback is a callback no matter what language you are developing in. I figured the possibility was worth exploring.
As it turns out, the news is good. You can multithread
all you want! Building a multithreaded application with Visual Basic 5.0 is pretty straightforward, but there are
a few issues that can trip you up. Visual Basic Books Online does a pretty good job of describing the new support
for windows subclassing and enumeration callbacks, but that is where the callback discussion stops. In this article, I will start out with simple samples that are easy to understand and build up to a multithreaded application that you might not expect to see written completely in Visual Basic: a Win32 debugger! When I mentioned earlier that Visual Basic lets you take full advantage of the Win32 API, I wasn't kidding. You'll also find that all of the code works on both Intel and DEC Alpha CPUs, including the debugger code.
What is AddressOf?
As the name implies, AddressOf is used to return the address of a Visual Basic procedure. The intent of this unary operator is to pass the address of a Visual Basic procedure to an API call out of a DLL such as EnumWindows. AddressOf has a couple of restrictions. Visual Basic won't let you take the address returned by AddressOf and call through it as you could with a pointer to a function in C. In addition, AddressOf can only be used in the arguments list for a function and cannot be used to just assign a variable. However, AddressOf is very powerful and relatively simple to use.
I wrote a simple program, EnumWnds, that demonstrates AddressOf. EnumWnds enumerates all the top-level windows using the API function EnumWindows with a callback function. When the Enumerate Windows button is pressed, the EnumWindows API function is called with the callback function WndEnumProc, and the listbox control is passed for the application-defined data. If the window has a title, EnumWynds inserts the title into the main form's listbox. Figure 1 lists the form code and code module that holds the callback function.
Dim bRet As Long
bRet = EnumWindows(AddressOf WndEnumProc, lstOutput)
Keep in mind that the procedure after the AddressOf can be either a function or a subroutine. It just depends on what the individual API requires. With EnumWindows, the window enumeration callback routine must continue to return 1 (TRUE in C/C++ terms) to continue to be called. If the callback returns zero (FALSE), then Windows will cease the enumeration.
The window enumeration callback is relatively simple in this example as well. It calls the Windows® API GetWindowText to retrieve the text from the passed-in window handle and displays the title if there is one. With the callback functions, it's a good idea to explicitly declare the ByVal and ByRef declarations so that you keep everything straight. In the EnumWnds example, if you leave off the ByVal declarations on WndEnumProc your program will crash, and will crash the IDE bugger as well because parameters will be treated as pointers instead of values.
Besides providing callback functions for enumeration APIs, AddressOf can be used to subclass window procedures. Subclassing a window means inserting your own window procedure in place of the original one so you can perform additional processing on the message stream. For example, subclassing is used to intercept the messages passed to an edit control so that the edit control can receive all the keyboard input when the edit control has focus. Microsoft Word subclasses the edit control where the user can type in the return address in the envelope dialog. By trapping the Enter key, the subclassing forces it to break the lines when the user is typing in a return address, just as the user would expect. If the return address edit control in the envelope dialog was not subclassed, the user would have to remember some strange keyboard combination for breaking the linesas well as almost always dismissing the dialog with the Enter key.
Now that you have an understanding of AddressOf, I will begin discussing some of the issues associated with multithreading. First, let's take a look at what multithreading cannot do: it cannot make your application instantly faster, it cannot solve bad programming practices, and it cannot cure world hunger. In fact, writing multithreaded applications is extremely difficult and nearly impossible to get right without careful planning and more testing than you ever dreamed of.
At the same time, when you need multithreading, you really need it. Many times, having a background thread handle an operation can make the difference between a decent application and a great one. And since 85 percent of all Visual Basic-based apps are written to access a database, opportunities to use multithreading are numerous.
For example, you can use multithreading to save data for printing into a temporary file and then print the file as a background thread. If you are writing a data analysis-intensive front end with huge server-driven SQL queries, you could use multithreading to keep the queries cranking in the background while allowing the user interface to remain responsive. If you are writing a field sales application that runs on a salesperson's laptop computer and uses RAS to connect to a central database, you could create a thread to run in the background that reserves a place in the queue for a new order. That way, when the salesperson is typing the final sales agreement, she can report to the customer when the order is expected to ship based on the most up-to-date information. Finally, being able to multithread makes it possible to create things like debuggers in Visual Basic. Since writing debuggers is what I do for a living, it gives me a quick prototyping tool that I have never had before.
There are a few rules to keep in mind when multithreading. First, only multithread if you have chunks of work in your application that can be done at the same time as other work. Second, do not multithread if you will end up disabling the entire user interface because it's waiting on the background thread. Remember, the point of multithreading is to let the user continue using your application for other things while the background thread is running. Third, you should double, maybe even triple, the expected development and testing time because there will be many subtle timing and synchronization problems. Fourth, run your multithreaded applications on as many machines as possible, including very fast machines and multiprocessor machines. When writing VBDebug, my Win32 debugger example, a nasty synchronization problem showed up only when I ran it on a 400Mhz DEC Alpha, even though I ran it successfully on a 200Mhz Pentium Pro. I also had nasty synchronization problems on a multiprocessor system. Finally, always assume that all the threads are running at exactly the same timeif there is an infinitesimal chance that they will deadlock, they will.
I can give you some basic guidelines about using multithreading, but I recommend learning more to really understand how multithreading works. Perhaps the best place
to start is Advanced Windows, Third Edition by
Jeffrey Richter (Microsoft Press, 1997). It has the most lucid explanation of multithreading for Win32 I have found. Also, it discusses multithreading and synchronization at the SDK API level, which is how it has to be used in Visual Basic. Another great resource is the Microsoft Developer Network CD.
Debugging and Native Code Generation
Before I can show you a simple Visual Basic-based program that does multithreading, there are three things that you must know about developing these types of applications. First, you cannot use the Visual Basic IDE to run or debug your application. If you try to run the programs I wrote for this article inside the IDE, it will crash. The Visual Basic Books Online mentions several times that the IDE cannot debug multithreaded applications, even single-threaded Visual Basic executables that use multithreaded ActiveX controls written in C++. To debug your app, you will need to set your projects to generate unoptimized native code and turn on symbolic debug information.
The fact that the Visual Basic IDE cannot handle multithreaded programs is not a design flaw. Remember, writing a multithreaded Visual Basic-based application that isn't a simple, non-UI ActiveX control is way beyond what Microsoft designed Visual Basic to do. As long as you can debug these applications once they are compiled to native EXEs, which you can, it's not really a big deal. Although this should take most of the sting out of not being able to use the Visual Basic debugger, the Visual Basic runtime is, from everything I can tell, pretty much thread-safe. If it wasn't, then you couldn't do multithreading at all without crashing every time.
The second issue that makes Visual Basic native EXE debugging difficult is that there seems to be a bug in the symbol table generation; sometimes the source line numbers do not align with the code being executed. If you can deal with your code being off a line or two in the debugger, this should not be too big a problem.
The last issue you should be aware of is that the sym-
bols generated for Visual Basic native executables are not always very descriptive. For the most part, there are no symbols for global variables, though most, but not all, of the local variables have proper symbols. To see the values of globals and those locals without symbols, I generally sprinkle calls to OutputDebugString liberally around the application. Keep in mind that the new Visual Basic 5.0 Debug object does not get compiled into your native applications, so you have to use conditional compilation for asserts and prints. Also, there are many temporary variables generated in Visual Basic code and they all get the same name: unnamed_var1. You can pretty much figure out which one applies to what section of the code by watching them change as you step through in the debugger.
Let's Start Simple
Now that you know what to watch out for when developing and debugging native code in Visual Basic, I can show you some multithreaded code. Figure 2 lists the code for MT.EXE, which is about as simple a Visual Basic-based program as you will find. It is simply a Sub Main that creates two threads that each show a message box (see Figure 3). Despite its simplicity, this sample demonstrates many of the issues associated with multithreading in Visual Basic.
The best way to see how MT.EXE works is to compile and run it. When you do, you will see the three message boxes pop up that indicate which thread the message box came from: two message boxes come from threads created in the code, and the other is the application main thread. To prove that each message box is from a different thread, each message box shows the thread identification for the thread in which it is called. If you really want to make sure that MT.EXE is creating threads, use the SDK program PVIEW.EXE to see what it reports.
Figure 3 Message Boxes
If you run MT.EXE a couple of times and try different shutdown scenarios, you will notice that shutting down the main thread message box first and leaving the other threads running causes all of them to end at once. While this may seem odd, it isn't when you know that the main thread eventually calls ExitProcess when it is shutting down. ExitProcess automatically kills any outstanding threads that the process still has running. Keep this in mind when developing your multithreaded programsmake sure to end your threads before the main thread exits or you could cause large resource leaks or, worse yet, database locks when TerminateThread is called on your threads.
In C/C++ development, there is no difference between functions or subroutines since creative casting can turn one thing into something else. However, with the extra-strict declarations of Visual Basic, I originally thought that only true Visual Basic functions could be used because there might be a difference internally between a function and a subroutine. As I discovered in MT.BAS, the AddressOf operator can pass both functions and subroutines to CreateThread or any other API.
Even though you can pass Visual Basic subroutines
and functions to CreateThread, keep in mind that one of
the main ways to tell that another thread finished properly is to check the thread exit code with GetExitCodeThread.
If you pass a Visual Basic subroutine to CreateThread,
the thread exit code will always be zero. If you pass a Visual Basic function to CreateThread, the return value of
the function will be the exit code of the thread. So, if you need to know the exit code, your thread routine needs to be a function.
If you look at MT.BAS, you'll see that I set up two ways of calling CreateThread. When creating the thread for the DaThreadFunc function, I use the straight CreateThread declaration to show how to pass the thread parameter by reference because I am passing a user-defined type to DaThreadFunc. For the most part, you will almost always be passing some sort of user-defined type by reference to the thread function to have it work on unique data. To make passing the thread parameter easy, lpThreadParameter is declared of type Any. As for DaThreadSub, it only needs a value, not a reference, so I used the CreateThread_
ByValParam declaration of CreateThread. While Visual Basic is extremely type-safe when doing Basic code, it fortunately allows you to coerce the different values to external DLL functions.
But Wait! There's More
Now that you have seen MT.EXE in action, you are probably salivating at the thought of making your Visual Basic application multithreaded. Before you end up with a 37-thread monster, there are still a few more things that you want to keep in mind when creating multithreaded Visual Basic-based applications. None of these minor issues are showstoppers, but they affect how you will utilize threads and classes in your application's design.
The first issue is a limitation of the AddressOf operator. AddressOf will only accept functions and routines that are part of a standard module, or code module. Simply put, your thread function must reside in a BAS file. This is not a big limitation, but it means you cannot have a helper function in a CLS module, for example, be the thread function. While you might want to do this so the thread function can access items in the class, it simply will not work and results in a compilation error.
As an alternative to having the thread function come out of a class module, you may have thought about using a class method as the threaded function. AddressOf will not allow this either. Since Visual Basic classes are COM objects, they conform to the COM binary standard. This means that the COM object is a pointer to a vtable so the address of a COM method is not valid. The same restrictions apply in C++: COM methods, or C++ methods for that matter, cannot be used as parameters to CreateThread.
It is perfectly legal to pass user-defined types or a specific type as the thread parameter. You have to be careful passing items as the thread parameter because some items cannot be used. For example, if you try to pass a Label control to a normal Visual Basic routine, you will get a warning at compile time. Since CreateThread uses As Any as the type for lpParameter, if you try passing a Label control, your app compiles but will crash when you run it.
What if you need to pass a Label control to a thread? Just wrap the Label control in a classVisual Basic classes work great as the thread parameter. Just keep in mind that you need to define the lpParameter variable to CreateThread as "ByRef lpParameter as Any" so you can pass any type of class into your thread function. If you design your application with care, you can even have an abstract class as the type your thread function takes. This lets you pass in polymorphic classes to do different levels of work. Before you can pass the class into CreateThread, however, you must create the class by using the New keyword. As you will see later, I use this heavily to do some extensible multithreading. Once the thread function starts, it can then call methods off of the class variable to do whatever you need to do in the thread.
As well as Visual Basic classes work as the thread parameter in multithreaded applications, they do not work at all if the class is declared WithEvents, one of the cool, new Visual Basic 5.0 features. While you cannot use events in your classes, you can use a workaround to get the same effect. I will show you how later.
In normal Visual Basic 5.0-based applications, you can implement components such as business rules with classes so that the user of the class can provide the user interface instead of having the component drag it around. Events make this possible by allowing the item that instantiates the class to declare it with the WithEvents keyword and by handling the various events inside the module that declares the class. If this is done in a form, then the form can provide the event handlers for the class and display the class's data. The Visual Basic Books Online has an excellent example of this where the class has a PercentDone event that it calls when the class is doing a lengthy operation.
Unfortunately, because the Visual Basic class that is declared through WithEvents does not seem to be a true COM object, you cannot pass a WithEvents-declared class as a thread parameter. I originally questioned the COM-ness of a WithEvents-declared class because there is no way to do events in straight C++ OLE developmentand it did not work when I tested it. EVENTPROBLEM.EXE, included with the source code for this article, shows what happens when a WithEvents-declared class is called from a thread.
In EVENTPROBLEM.EXE, the WithEvents-declared class is TheClass. When the CallMeFromTheThread method is called, it displays a message box indicating that it was called and then calls RaiseEvent on the DoTheEvent event. In the main form, there are two different instances of this class declared: g_ClassWithEvents has events and g_ClassNoWith does not. When the Declared WITHOUT EVENTS! button is pressed on the form, a thread is created with the function TheThread, and the thread just calls CallMeFromTheThread on the class. Since the g_ClassNoWith variable is not declared as WithEvents, the message box in the class shows up but no event can be called. When the Declared With Events button is pressed, the g_ClassWithEvents variable is passed to another thread. When that thread tries to access the thread parameter, there is a runtime error that reports "Object variable or With block variable not set," and no functions can be called on the class that raises events.
To try getting the WithEvents-declared class in EVENTPROBLEM.EXE to work, I created a global variable of the same type and then did a Set operation to take care of
the assignment of the thread parameter. I discovered, however, that Visual Basic won't let you declare an object variable WithEvents in a standard module, even as a local variable. Just accessing the thread parameter that has a WithEvents-declared class variable in it causes an exception to be thrown. WithEvents and multithreading do not mix, so you lose one of the best benefits of the new Visual Basic capabilities.
Fortunately, although not as elegant as class events, there is still a way to isolate the output for the class that is passed as the thread parameter. The idea is similar to that of events: when the class needs to show some output, it needs to tell something else to handle the output. Obviously, this something else should be capable of outputting information in many different ways.
The idea is to have an abstract class as a public member of the class. This class is what you would normally handle with events. For the most part, this is where you would handle the entire user interface, and the class, instead of raising an event, would call through the abstract base class for all of its output. Classes using this abstract output class can be passed safely to CreateThread as the thread parameter. This is an acceptable solution, but you must carefully define the abstract output class so that it will meet the future needs of the main thread parameter class, just like you would have to do when designing and using events. Figure 4 shows the code for EVENTSOLN.EXE, which illustrates the implementation of an output class and its use to show the output.
Some More Complex Code
Now that you've seen a simple example of a multithreaded Visual Basic application and you know what it takes to pass classes to your thread function, it's time to look at something a little more interesting. Granted, THREADTEST.EXE is not the most exciting program in the world, but it gives you an idea of a real-world implementation (see Figure 5). THREADTEST.EXE is a simple timer application that increments a label control every 100 milliseconds in a background thread (see Figure 6). You can start, pause, resume, or end the background thread completely through synchronization events just like you would do in your own applications. While the average programmer could whip the same program out in a couple of minutes with a timer control, using a thread is much more interesting.
Figure 6 THREADTEST.EXE User Interface
Needless to say, the user interface for THREADTEST.-EXE is pretty intuitive. Just click the four buttons on
the bottom of the window to create and control the background thread, and click on the End button to close the application. To really see what THREADTEST.EXE is doing, you might want to run it under a debugger to see what is executing when.
I applied some of the lessons that I learned from MT.EXE to THREADTEST.EXE. For instance, when I start the background thread function, TheThreadSub, I pass a class of type LabelClass as the thread parameter. The LabelClass has a single public property that is a Label object. If this were a real-world program, I would not use the Label object directly but would have provided wrapper functions to set and get the Caption property. The one instance of the LabelClass is instantiated in the Form_Load event for the main form.
The most interesting part of THREADTEST.EXE is how the main thread controls the background thread. In this program, after the background thread is started, it just runs until the main thread tells it to pause or stop. If the background thread is paused, then the main thread tells it to either restart or stop. In both states, running or paused, it's vital that you are able to stop running the background thread from the main thread. The pause and restart notifications are a lower-level toggle item.
To make the background thread behave in this manner, you might be inclined to create two carefully controlled global variables. Since the background thread is always supposed to be running, it would need a While...Wend loop to run infinitely. Moreover, since the stop notification is so important, the top of the loop after the While would have to check the g_bStop Boolean flag to see if it was true. If g_
bStop was true, then you would exit the subroutine. If g_bStop was false, then you could check the value of the g_bPaused Boolean flag. If g_bPaused was false, then you could go on and update the label control. The following pseudo code shows how the thread function might look:
Public Dim g_bStop As Boolean
Public Dim g_bPaused As Boolean
Public Sub GlobalVarThread (clsLabel as LabelClass)
if (True = g_bStop) then
if (False = g_bPaused) then
' Do the update stuff here.
While the global variable approach seems to work at first glance, there are a couple of problems with it. First, the background thread sits in a tight loop, polling away at variables, which eats up a great deal of CPU time. This would become obvious if you ran the application because the TaskManager in your iconbar would jump to fully green, indicating something is sitting in a tight loop. |
The second problem is much more serious and not as obvious. When you start the background thread, you need to set the state of both global variables to False so you can restart the thread after you stop it. Consider what would happen in this scenario. First, you press the Stop button (which sets g_bStop to true). Second, Windows NT® immediately task-switches away to service a huge network packet. Third, you are fast on the mouse and click on the Start button. Fourth, Windows NT task-switches back to your application and executes the main thread, which causes the Start button press to be handled. At this point, something really nasty would happen; a background thread would be created from pressing the Start button, your original background thread would spin around and see that g_bStop is true (remember, g_bStop is set to false by the Start button press), and you would have two background threads running when you should only have one.
As you can see, polling global variables would be a mess and would cause even more problems if the application ran on a multiprocessor machine. But fortunately, there is another way to signal the background thread that some event has taken place: use Win32 event objects as your synchronization objects. This is what I did in the THREADTEST.EXE program.
Events are created by calling the CreateEvent API. Each thread that needs an event must call CreateEvent because event objects cannot be passed around. Both threads must be working with the same event, so the lpName parameter to CreateEvent lets you give the event a name. Note that events are cross-process, so be very careful and use event names that are unique. A good technique is to append the process ID to the event, guaranteeing its uniqueness.
Event objects have two states, signaled and nonsignaled. I liken a signaled event to a thing that has happened or is being set to true. The CreateEvent API has other parameters that let you specify the initial state and whether the event is manual-reset or autoreset. A manual-reset event means that, when you cause an event to be signaled, you must explicitly set it back to nonsignaled. With autoreset events, when you signal an event, the event is automatically set to nonsignaled when a single waiting thread has been released. To set a manual-reset event to the signaled state, call SetEvent on the event handle. To set a manual-reset event to the nonsignaled state, call ResetEvent with the event handle.
Once you have created an event, you need to know when it is signaled. The API function WaitForSingleObject indicates if an object is signaled, and will also wait a specific period of time before timing out. Of course, the value INFINITE can be passed to WaitForSingleObject to have the thread block forever until an event is signaled.
If you need to wait for a set of events, WaitForMultipleObjects takes an array of event handles and tells you which event was signaled or if all events in the array were signaled, depending on the bWaitAll parameter value. For both functions, if the event or events are not signaled, the time slice for the thread is released. This prevents polling from wasting CPU time. Finally, when you're done with the event, which is a ubiquitous Win32 handle, call CloseHandle on it to free the event.
In THREADTEST.EXE, the synchronization scenario is that stop state must always happen whether the thread is paused or running, and the paused state must toggle back to the running state. Like the global variable attempt, THREADTEST.EXE has two events. As it has only two threads, there will not be many threads waiting on the events, so I created them both as manual-reset events so I could control the signaled state myself.
Since the background thread will be waiting on the state of two events, I keep the events in an array two elements long. The stop event is higher priority for THREADTEST.EXE, so it is the first element in the array and the pause/running event is the second. The WaitForMultipleObjects function treats the items in the event array parameter in decreasing priority the higher you go in the array. This means that, even though the event in the 99th index is signaled, WaitForMultipleObjects will return the lower number if any of the previous position's events is signaled.
The only thread that actually waits on the events is the background thread. Since it needs to update the label control every 100 milliseconds, the background thread will always be running unless it is paused. To accomplish this, the stop event is created as nonsignaled, but the pause/running event is created as signaled. By keeping the pause/running event in the signaled state, WaitForMultipleObjects always returns immediately so the label control can be updated.
You are probably wondering why I have only two events for basically three statesstopped, running, and paused. The idea is that the paused and running states are Booleans; either the background thread is running or not. If you had three events, you would end up polling because, if the paused event was signaled, you would have to set the running event to nonsignaled. Since WaitForMultipleObjects would return on the signaled pause event, you are doing glorified polling. When the background thread is paused, you want to get into an idle state so that it does not take up any CPU time until it is supposed to be running. Of course, if the paused/running event is always nonsignaled, the background thread stays in WaitForMultipleObjects until either of the events becomes signaled, effectively pausing the thread.
The main thread in THREADTEST.EXE is responsible for creating the background thread and for signaling the appropriate events based on user input. Inside the form, the main thread keeps the state of the thread because the Pause Thread button doubles as the Resume Thread button. When the Pause Thread button is pressed, the main thread calls ResetEvent on the paused/running event, which kicks the background thread into the WaitForMultipleObjects call until something else is set. When the Resume Thread button is pressed, the main thread calls SetEvent on the paused/running event to get it back to the signaled state.
When the main thread handles the Stop Thread button, it does more than set an event in the KillTheThread function. After setting the stop event, the main thread calls WaitForSingleObject, using the background thread handle to wait for the thread to end when the thread handle is signaled. Remember that there are many different handles that WaitForSingleObject can wait on, not just event object handles. I probably do not need to wait for the thread in THREADTEST.EXE, but in real-world applications you will need to wait to make sure the background thread shuts down before you end the application. If the background thread is doing something important (like shutting down a SQL database connection) and you let the main thread call ExitProcess, you can kill the thread in the middle of the shutdown and lose data or corrupt the database.
Some Final Visual Basic Multithreading Issues
There are a couple of issues that I need to cover before I introduce the bigger debugger example. First, you should be certain that the Visual Basic runtime is thread-safe. As I mentioned earlier, the Visual Basic runtime is mostly thread-safe. While I did not sit down and completely disassemble MSVBVM50.DLL, the fact that Visual Basic 5.0 now supports apartment-model multithreading in ActiveX controls means that at least the nonuser interface portions are completely thread-safe. Since the programs in this article only use MSVBVM50.DLL; however, I cannot vouch for the thread-safety of the other Visual Basic runtime DLLs, such as VB5DB.DLL. If your multithreaded app needs to use some other Visual Basic system DLLs or a multithreaded user interface, you will need to do some prototyping to determine that everything is working correctly.
If the runtime is thread-safe, the next thing to check is the p-code interpreter. The interpreter doesn't appear to be thread-safe because a multithreaded application compiled to p-code eventually crashes. Maybe future versions of Visual Basic will allow multithreaded p-code compilations.
Finally, while you might want to use multithreading in your Visual Basic AddIn, the IDE only supports a single execution threadyour AddIn cannot be multithreaded.
The Next Step
So far, I've covered the basic elements needed to write multithreaded applications with Visual Basic 5.0. In this part, I took a close look at the AddressOf operator, and put it to use creating threads. But so far, my samples have been small. In the second part of this article, I'm going to write a real-world multithreaded application in Visual Basic: a fully functional Win32 debugger shell. While it won't have a symbol engine or offer breakpoints, it isn't that far from plugging them in. Stay tuned.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.