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.

T
he ease of multithreading in Xbase++ applications is one of the language’s 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 thread’s :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.

     That’s all there is to it.  We have covered the rudiments of running a dialog window in its own thread.  Now let’s examine the source code in more detail, and then run the application to see what’s 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:
      aadd(aThreads,oThread)
Next, the final lines of code show the logic for evaluating the parameter cMode in order to start the appropriate routine. This logic allows LaunchPad() to be used as a system-wide starting point for all threaded routines.
     IF cMode=='test'
      oThread:start( "testwin",'Wow!')

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 window’s 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 object’s :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 thread’s methods and instance variables.  Our usage here returns the numeric ID number from the current thread’s :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 window’s 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 thread’s :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 key’s closing did not allow the program to exit the window’s 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 window’s 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 doesn’t 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
nX := (aRect[1]- nWidth)/2 // horizontally centered
nY := 28 // bottom - just above taskbar

******** Create application window
oDlg := XbpDialog():new(,,{nX,nY},{nWidth,nHeight},,.T.)
oDlg:title := 'Dialog Windows in Recycled Threads'
oDlg:border := XBPDLG_SIZEBORDER
oDlg:tasklist := .T.
oDlg:close := {|| Enditall() }
oDlg:create()

******* Create menu
MenuCreate(oDlg)

******* set focus to application window
SetAppWindow(oDlg)
SetAppFocus (oDlg)

******* primary event loop
DO WHILE nEvent <> xbeP_Quit
nEvent := AppEvent(@mp1,@mp2,@oXbp)
oXbp:handleEvent(nEvent,mp1,mp2)
ENDDO

RETURN .T.
* End of FUNCTION main()
***********************************************************************

********************************************************************
*                         FUNCTION Enditall()
* Routine to terminate the program
********************************************************************
FUNCTION Enditall()
CLOSE ALL
QUIT
RETURN .T.
* End of FUNCTION Enditall()
***********************************************************************


***********************************************************************
*                          FUNCTION menucreate()
* Called by main()
***********************************************************************
FUNCTION menucreate(oDlg)
LOCAL oMenuBar,oSubMenu

******* create main menu bar
OMenuBar := oDlg:menuBar()

******* create 'files' sub-menu
oSubMenu := XbpMenu():new(oMenuBar):create()
oSubMenu:title := "~Files"

******* add sub-menu items
oSubMenu:addItem( {"~Thread Window", {||LaunchPad('test')} } )
oSubMenu:addItem( {"E~xit", {|| Enditall()} } )

******* add sub-menu to main menu bar
oMenuBar:addItem({oSubMenu,nil})

RETURN .T.
* End of FUNCTION menucreate()
***********************************************************************

 

***********************************************************************
*                         FUNCTION LaunchPad()
* Called from main menu
***********************************************************************
FUNCTION LaunchPad(cMode)
LOCAL oThread,i

******* Create thread object
oThread := Thread():new()

******* Add to Public array - for Demo purposes only !!!!!!!!!!
aadd(aThreads,oThread)

******* Launch thread
IF cMode == 'test'
   oThread:start( "testwin",'Wow!')
ELSEIF cMode == 'somethingElse'
   oThread:start( "whatever")
ENDIF

RETURN nil
* End of FUNCTION LaunchPad()
***********************************************************************

 

***********************************************************************
*                           FUNCTION TestWin()
* Called from LaunchPad()
* Test bed for experiments - runs in its own thread
***********************************************************************
FUNCTION TestWin(cMsg)
LOCAL oDlg,oParent
LOCAL aRect,nWidth,nHeight,nX,nY
LOCAL nEvent,mp1,mp2,oXbp

*********************************** Set up window
******* get main app window object
oParent := SetAppWindow()

******* get dimensions of drawing area
aRect := oParent:drawingArea:currentSize() // {width,height}

******* set dimensions for this window
nWidth := 450
nHeight := 200

******* Starting Coords - calc for centering
nX := (aRect[1] - nWidth)/2
nY := (aRect[2] - nHeight)/2

********************* Create window
oDlg := XbpDialog():new(oParent:drawingArea,,{nX,nY},{nWidth,nHeight} )
oDlg:title := space(3) + cMsg + " A window running in thread " + ;
                 ltrim(str(ThreadObject():threadId ))
oDlg:close := {|mp1,mp2,obj| obj:destroy() }

******* For Demo purposes !!!!!!!!!!!!!
oDlg:keyBoard := {|nKey,uNil,obj| ;
              iif(nKey == xbeK_ESC,obj:destroy(),nil)}

*oDlg:keyBoard := {|nKey,uNil,obj|iif(nKey == ;
*                    xbeK_ESC,CloseTestWin(oDlg),nil)}

oDlg:create()

******* program control
setAppFocus(oDlg)

******* This is necessary to run in a thread
nEvent := 0
DO WHILE nEvent <> xbeP_Close
   nEvent := AppEvent( @mp1, @mp2, @oXbp )
   oXbp:handleEvent( nEvent, mp1, mp2 )
ENDDO

RETURN nil
* End of FUNCTION TestWin()
***********************************************************************

 

***********************************************************************
*                           FUNCTION CloseTestWin()
* Called by TestWin()
***********************************************************************
FUNCTION CloseTestWin(oDlg)
oDlg:destroy()

******* Manually deactivate thread object
ThreadObject():quit()

RETURN nil
* End of FUNCTION CloseTestWin()
***********************************************************************

Copyright © 1999-2006 Clayton Jones
All rights reserved.

Return to: Articles Page |Top-Down Page | Software And Services | Home