First in a two part series Copyright © 1999-2006 Clayton Jones |
by Clayton
Jones |
Note: Complete source code for
the demonstration program described in this article can be found at the bottom of this
page. It can be copied and pasted into a prg and compiled. The ease of multithreading in Xbase++ applications is one of the languages powerful features. With as few as two lines of code the developer can completely encapsulate routines which are able to run simultaneously without interfering with one another. All of the complex memory management tasks are done automatically "under the hood". The potential for making easy work out of complex event-driven database programming is enormous. In my own experimenting I have discovered some undocumented things which I think are important for anyone wishing to use threads for practical database related tasks. This first of two articles will present some of these, with a special emphasis on understanding the re-use or recycling of threads and how it affects memory use. NOTE: This material is presented with the assumption that the reader is at least familiar with the Xbase++ documentation on multithreading. The included source code for this project is ready to compile, and has been tested in Windows 95, 98 and NT 4.0. |
A Dialog Window in a Thread |
One of my prime interests has been the prospect
of running a dialog window in its own thread. Although the thread examples in the Xbase++
documentation and source code samples do not show an example of doing this, they do reveal
enough clues to get started. The first thing to notice is that starting a thread is always
done from a "parent /child" point of view. We cannot have a routine which
creates its own thread for itself. We must create a thread and then send it off on
its own to do something while we go about our business. There is always an event
loop in the "parent" thread to which the application control returns after
starting the "child" thread. Dialog windows are typically launched from a menu item or pushbutton action codeblock. In a large database application, it helps to have a central location from which all the thread launching can be managed. I like to use an intermediate function which can be called by the menu to do the work, and from which the application control can RETURN to the primary event loop. |
The Launch Pad |
The function LaunchPad() accomplishes this in
our example. It provides a platform from which many routines can be launched in
individual threads. LaunchPad() is called via the menu item codeblock, oSubMenu:addItem({~Thread Window,{||LaunchPad('test')}}) and contains a single parameter (test) which is a flag used to indicate which routine to start in a thread. Within LaunchPad() a new thread object is created and assigned to a variable. oThread := Thread():new() Then the threads :start() method is called. It contains a string parameter, TestWin, which is the name of the function or procedure to be run in the thread. Our example also shows the use of a second argument, Wow!, which becomes a parameter in the call to TestWin(), the equivalent of TestWin(Wow!). oThread:start(TestWin, Wow!) Application control then RETURNs from LaunchPad() to the primary event loop, and TestWin() goes on its merry way. |
The Dialog Window |
The function TestWin() contains the code to
create and display a simple XbpDialog window. The most important feature is the
presence of its own event loop. The dialog will not function in its own thread
without it. This event loop becomes, in effect, a primary event loop for the new
thread. Thats all there is to it. We have covered the rudiments of running a dialog window in its own thread. Now lets examine the source code in more detail, and then run the application to see whats going on under the hood with these thread objects, how they use memory, and how they are recycled. We will use LaunchPad() and TestWin() to demonstrate these basic principles. |
The Main() Function |
Looking at the starting function Main() we see a fairly generic routine which creates an application dialog window with a menu, and contains the primary event loop. Aside from these, note the PUBLIC array "aThreads". This is for demonstrating some aspects of thread recycling. The next function, Enditall(), is used to close the application. It is assigned to the :close event and is also called from the "Exit" menu item (Note: all of the dialog setup code is in Main() rather than Appsys() so it can be seen in the debugger). The following routine, MenuCreate(), also called by Main(), contains the standard Xbase++ menu routines. Notice the "Thread Window" menu item which calls LaunchPad(), and the "Exit" item which calls Enditall(). |
LaunchPad() |
In LaunchPad() notice that after
the thread object is created and assigned to the LOCAL oThread, the object is added to the
public array aThreads which was declared in Main(). This will allow us to view the
object in the debugger: |
TestWin() |
TestWin() contains the code for a very simple
child dialog window. Some code is commented out for now, and will be used later in the
demo. Notice the line which defines the windows title: oDlg:title := space(3) + cMsg + " A window running in thread " + ; ltrim(str(ThreadObject():threadID )) It uses the parameter cMsg ("Wow!") sent in from LaunchPad(), and also uses the Xbase++ function ThreadObject(). Xbase++ assigns a sequential number to each thread object as it is created. It is contained in the objects :threadID instance variable. ThreadObject() returns a reference to the thread object currently executing code, and allows an encapsulated routine to obtain a reference to the thread it is running in. It can then be used for accessing the threads methods and instance variables. Our usage here returns the numeric ID number from the current threads :threadID ivar, and we will use it to help show how the threads are being handled by the operating system. Each window we create will display its thread ID number. This is shown in the screen shot in Figure 1. |
Figure 1 |
Notice that TestWin()s event loop is defined to terminate when a
close event is generated, DO WHILE nEvent <> xbeP_Close and that the :destroy() method is assigned to the :close callback slot. This is activated by clicking the windows close button which generates the close event. Next, notice that there are two versions of the :keyboard callback slot, one of which is commented out. One calls the :destroy() method directly, and the other calls CloseTestWin(), a routine which does several closeout chores for the window. Both versions allow closing the window by pressing the Esc key. The difference in their behavior brings us to a key point of this discussion. |
A Key Point |
Every thread has an :active instance variable. When code in the thread begins executing it is automatically set to .T. The Docs state that when the code has finished executing the :active ivar is automatically set to .F. However, if we close the window without exiting the event loop and executing the RETURN command, :active remains .T. Notice in our code that when we click the close button a close event is generated. The :close event is handled, program flow exits the event loop and RETURNs, and the threads :active ivar is set to .F. When we close the window via the Esc key, however, the dialog is destroyed directly, without generating a close event, and the program flow does not exit the event loop. The result is that :active remains .T. This can be easily seen in the debugger in Experiment 1. |
Experiment 1 |
Compile the app as is and run it with the
debugger. Start two instances of the thread window, and see how they are numbered 2
and 3 (the application is running in thread 1). Close window 3 using the close
button, and window 2 by pressing Esc. Since we assigned the thread objects to the
public array aThreads in LaunchPad(), we can now examine them in the debugger, even though
the windows are closed. The array contains two objects. Examining their
:threadID and :active ivars reveals that #2 is still active, and #3 is not active.
Thread 2 is still active because the Esc keys closing did not allow the program to
exit the windows event loop. Open another thread window (notice that it is number 4). Place a breakpoint on TestWin()s RETURN command, resume execution, and then click the windows close button. When the debugger reopens on the RETURN line, examine the thread object in the array (it will be the third element). It should be ID number 4, and :active is still .T. Now press F10 to execute the RETURN. Program control goes back to the primary event loop. Now examine thread 4 and see that :active is now .F. Here is a key point: if you close a threaded window without exiting its event loop, you must manually de-activate the thread using the :quit() method. This can be seen in Experiment 2. |
Experiment 2 |
Go back into the TestWin() code, comment out the
first :keyboard line and uncomment the second one, which calls CloseTestWin().
Notice in CloseTestWin() that after destroying the dialog we again use ThreadObject() to
gain a reference to the current thread, this time to manually call its :quit() method. ThreadObject():quit() Recompile, run with the debugger, and place a breakpoint on this line. Open a thread window and then close it by pressing Esc. By examining the thread object before and after this line executes, we see the thread become inactive. Why is this so important? Because a thread cannot be recycled if it remains active after being used. |
Thread Recycling and Memory |
Every time we create a thread object some memory is used (RAM, not Resources). This memory is released after the thread becomes inactive. The operating system appears to reuse any existing inactive thread objects for newly created threads, or at least recycles the thread ID numbers. Since there are no :create() and :destroy() methods for the thread class, it is not known for certain at this time whether the object itself remains in existance and is reused (as suggested in the descriptions of the :destroy() methods for Xbp classes), or whether it is removed from existance and just the thread ID number is reassigned. For our practical purposes it doesnt really matter. The important point for us is that the thread must become inactive after we use it so it can be recycled and its memory released. We can demonstrate this in experiment 3. |
Experiment 3 |
In LaunchPad(), comment out the line that
assigns the thread object to the array aThreads (the object cannot be recycled while a
reference to it is being held in the array, even though it may be inactive this is
why, in experiment 1, the new window was #4). Also change the :keyboard callback
routine back to the one which calls :destroy() directly, and then recompile. Open three windows (2,3, and 4). Close 2 and 4 via the close button, and 3 via the escape key, which leaves its thread active. Wait five to seven seconds (if you begin reopening the windows before the OS has returned the previous memory, a new thread will be created), then open several thread windows again, and notice that thread #3 is not reused. If you continue opening and closing windows, 3 will never be reused. It remains active, in memory, with no way for us to deactivate it. |
Conclusion |
There is no doubt that the ease of
multithreading in Xbase++ is a major feature of the language. Running a dialog
window in its own thread is likewise a simple matter. As long as we are careful to
ensure that the thread is deactivited when closing the window, all thread memory will be
released, and its thread object (or ID number), will be recycled. The next article in this series will demonstrate the advantages of using threaded dialog windows for data entry routines and other database related activities. It is here that the huge potential for using threads in event driven database applications is fully realized. |
Complete Source Code - Ready To Compile |
*********************************************************************** * DIALOGS IN RECYCLED THREADS * Copyright (c) 1998,1999 Clayton Jones * Contents: * AppSys * Main * EndItAll * MenuCreate * LaunchPad * TestWin * CloseTestWin *********************************************************************** #include "Appevent.ch" #include "Xbp.ch" *********************************************************************** * FUNCTION AppSys() *********************************************************************** FUNCTION AppSys() RETURN .T. *********************************************************************** * FUNCTION main() *********************************************************************** FUNCTION main() LOCAL nEvent, mp1, mp2,oDlg,oXbp LOCAL nX,nY,nWidth,nHeight,aRect ******* This is for demo purposes only !!!!!!!!!!!! PUBLIC aThreads := {} ********************* Set up the application window ******* dimensions - almost full screen aRect := AppDesktop():currentSize() // {800,600} or {600,480} , etc. nWidth := aRect[1] - 4 nHeight := aRect[2] - 30 ******* starting coords ******** Create application window ******* Create menu ******* set focus to application window ******* primary event loop RETURN .T. ********************************************************************
******* create main menu bar ******* create 'files' sub-menu ******* add sub-menu items ******* add sub-menu to main menu bar RETURN .T.
*********************************************************************** ******* Create thread object ******* Add to Public array - for Demo purposes only !!!!!!!!!! ******* Launch thread RETURN nil
*********************************************************************** *********************************** Set up window ******* get dimensions of drawing area ******* set dimensions for this window ******* Starting Coords - calc for centering ******* For Demo purposes !!!!!!!!!!!!! *oDlg:keyBoard := {|nKey,uNil,obj|iif(nKey == ; oDlg:create() ******* program control ******* This is necessary to run in a thread RETURN nil
*********************************************************************** ******* Manually deactivate thread object RETURN nil |
Copyright © 1999-2006 Clayton Jones |
Return to: Articles Page |Top-Down Page | Software And Services | Home