Writing Multithreaded Graphics Programs

by Kelvin R. Lawrence

In previous issues of The Developer Connection News, we have looked at various aspects of the Graphical Programming Interface (GPI), including drawing widelines, producing simple business graphics, and working with transforms. Given these basic building blocks, the next step in optimizing the GPI is to add a second thread to the application. This article describes how to construct a two-threaded GPI application that does intensive drawing, while remaining responsive to user input.

Note: All of the previous issues of The Developer Connection News can be viewed from your Developer Connection CD-ROM using the Developer Connection Browser.

As a rule, all Presentation Manager (PM) applications should contain at least two threads of execution. The main thread handles the window procedure and processes user-generated events, while dispatching lengthy tasks to the worker thread. This rule is particularly true for graphics applications.

No one best way exists to code a two-threaded application. You could ask six different programmers for their favorite way to manage a two-threaded application and get six different answers. We will focus on a method of thread management that centers around the use of PM message queues and introduces two lesser known functions WinPostQueueMsg and WinCancelShutdown.

Being a Good Citizen
Well-behaved applications stay responsive to the user by reading their message queues often and handling time-consuming tasks on secondary threads. The key to writing a well-behaved PM application, and being a good citizen in the system, is the use of threads. A well-written PM application will dedicate one of its threads to the business of receiving events, dispatching them to the window procedure, and fetching the next event in a timely manner. Likewise a well-written window procedure will dispatch all lengthy tasks to a secondary, or worker, thread and return quickly.

We could dedicate many pages to a discussion of how the PM event model works and why things are the way they are. However, the goal of this article is to show how using a second thread lets a PM application draw intensively to the screen - while remaining very responsive.

Rotline (A Rotating Lines, and Other Shapes, Program)
Rotline is a two-threaded, menu-driven, PM application that lets the user select one of five drawing options spirals, expanding cubes, rotating cubes, multiple polygons, or rotating text. It uses a second thread to do the drawing, while the main thread receives user input The user can stop drawing and change the shape being drawn at any time. As the program source is quite lengthy, only some of the major functions are actually shown in this article. The full source and executables for the program are provided on your accompanying Developer Connection for OS/2 CD-ROM in the "Source Code from The Developer Connection News" category.

Creating the Worker Thread
The first thing the program does, even before creating its frame window, is to create its secondary, or worker, thread. This is done using a call to DosCreateThread. The program launches the ThreadProc function on the second thread with a stack size of 8192 bytes. This is the minimum size that is recommended for any application thread making PM calls. After the application creates the secondary thread, the main thread waits for the second thread to initialize before continuing. After the second thread has initialized, the main thread continues. Then, the application creates the frame and client windows and starts the message dispatch loop.  //- // main procedure //  --> Initialize PM for this process //  --> Create our message queue //  --> Create frame and client windows //  --> Show the window //  --> Enter our message dispatching loop //--

void main(VOID) {  HWND hwndFrame;  // Frame window handle HWND hwndclient; // Client window handle QMSG qmsg;

ULONG flCreateFlags =  FCF_BORDER     | FCF_SHELLPOSITION | FCF_TASKLIST  | FCF_TITLEBAR      | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX       | FCF_MENU ;

// Create our worker thread Once created the thread will sit waiting // for messages to be posted to it Create the second thread before // we create our windows so that it is ready to start servicing paint // requests when the windows are created

DosCreateThread( tid                 , (PFNTHREAD)ThreadProc                  , 0L                  , 0L                  , 8192L                  ); // Give the thread a timeslice before we proceed so it can get established

while( !hmq2 ) {    // Wait for the second thread to initialize DosSleep( 0 ); }; // Complete source code can be found on your // Developer Connection for OS/2 CD-ROM  ''Sample Code 1. Creating the worker thread''

The Worker Thread
The worker thread centers around the ThreadProc function. All threads that perform any PM functions must initialize PM for that thread and create a message queue. Therefore, the first thing that ThreadProc does is call WinInitialize and WinCreateMsgQueue</tt>. Having done this, it calls the WinCancelShutdown</tt> function. The purpose of WinCancelShutdown</tt> is to tell PM not to post a WM_QUIT</tt> message to this thread's message queue at system shutdown time. This is particularly important for threads that never actually read their message queue, but need one to perform PM function calls ThreadProc</tt> does check its message queue for activity, but calling WinCancelShutdown guarantees that the main thread will get the shutdown notification and be able to administrate closing down the worker thread.

In OS/2 Version 2.0, it was possible that the system would hang at shutdown time because of threads that did not call WinCancelShutdown</tt> and did not process their message queues. All subsequent releases of the operating system post the WM_QUIT</tt> message to the main thread first. It is, however, very good practice for all PM worker threads that have message queues to call WinCancelShutdown</tt>, unless they intend to do real work at shutdown time.  // // ThreadProc // VOID ThreadProc( LONG arg ) {  LONG  i;   QMSG  qmsg2; HWND hwnd2;

hab2 = WinInitialize( 0UL ); hmq2 = WinCreateMsgQueue( hab2, 0UL );

WinCancelShutdown( hmq2, TRUE );

qmsg2 msg = 0;

while( qmsg2 msg != UM_EXIT ) {    switch( qmsg2 msg ) {      case UM_PAINT {        hwnd2 = (HWND)qmsg2 mp1; DoPaint( hwnd2 ); }      break;

case UM_AUTO {        hwnd2 = (HWND)qmsg2 mp1; WinSetWindowText( WinQueryWindow( hwnd2, QW_PARENT), Thread busy        for( i=0; i < MAX_LOOP; i++ )         {           if ( i % REPAINT_THRESHOLD == 0 )           {             WinInvalidateRect( hwnd2, NULL, FALSE );           }

DrawShape( hwnd2 ); qmsg2 msg = 0; WinPeekMsg( hab2, qmsg2, 0L, 0L, 0L, PM_NOREMOVE ); if ( qmsg2 msg == UM_STOP || qmsg2 msg == UM_EXIT ) {            i = MAX_LOOP ; }          else if ( qmsg2 msg == UM_PAINT ) {            WinPeekMsg( hab2,  qmsg2, 0L, 0L, 0L, PM_REMOVE ); DoPaint( (HWND)qmsg2 mp1 ); }        }

if ( qmsg2 msg == UM_STOP ) {          WinSetWindowText( WinQueryWindow( hwnd2, QW_PARENT), Thread waiting ); }      }       break; }    WinGetMsg( hab2,  qmsg2, 0L, 0L, 0L ); }

if ( hmq2 ) {    WinDestroyMsgQueue( hmq2 ); }

if ( hab2 ) {    WinTerminate( hab2 ); }

DosExit( EXIT_THREAD, 0L ); } </PRE> ''Sample Code 2. ThreadProc function''

The Main Thread Window Procedure
The client window procedure for the main thread, ClientWndProc</tt>, is small because all it must do is either:
 * Receive events, and as appropriate, call WinDefWindowProc</tt> for default window processing to occur by the system.
 * Post messages to the second thread telling it to do some work for the few messages that it is interested in.

This enables the window procedure to process messages very quickly. While the actual task of drawing, painting the client area, or whatever might actually take the second thread a long time, it is not using up much of the main window procedure's time. This means that the application, and for that matter the whole desktop, stays responsive to the user - and that is our goal.  //-- // Client window procedure //--

MRESULT EXPENTRY ClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2 {  static HWND hwndMenu;   switch (msg)   {     case WM_COMMAND      {       switch( SHORT1FROMMP( mp1 ) )       {         case IDM_EXIT          {           WinPostMsg( hwnd, WM_CLOSE, (MPARAM)0L, (MPARAM)0L );         }         break; // Complete source code can be found on your // Developer Connection for OS/2 CD-ROM </PRE> Sample Code 3.

Communicating with the Worker Thread via WinPostQueueMsg
The main thread communicates with the worker thread by posting messages directly to the worker thread s message queue using the WinPostQueueMsg</tt> function. This is a nice way of communicating between threads using a PM interface; it does not require the existence of either real or object windows on the second thread. The main thread tells the worker thread what it needs to do by posting it one of four messages that the program defines.  </PRE> Sample Code 4.
 * 1) define UM_AUTO  WM_USER
 * 2) define UM_EXIT  WM_USER+1
 * 3) define UM_STOP  WM_USER+2
 * 4) define UM_PAINT WM_USER+3

The worker thread, once it is initialized, sits waiting for work It does this by calling WinGetMsg</tt> for its message queue. When nothing is in the queue, that is, nothing for it to do, the thread will block until WinGetMsg</tt> returns.

The application posts the <tt>UM_AUTO</tt> message to tell the thread to start drawing the currently selected graphical shape or shapes. Upon receipt of a <tt>UM_AUTO</tt> message, ThreadProc starts a lengthy drawing loop. The shape that gets drawn depends on the current user selection Each time around the loop, the application calls <tt>WinPeekMsg</tt> to see if a <tt>UM_EXIT, UM_STOP,</tt> or <tt>UM_PAINT</tt> message has been posted and takes appropriate action. By doing this, the second thread remains responsive to its master, that is, the main thread. Ultimately, the program appears very responsive to the user. The window procedure posts these messages as follows:
 * <tt>UM_EXIT</tt> when the program is about to exit This gives the worker thread the chance to shutdown cleanly before the program ends.
 * <tt>UM_STOP</tt> to interrupt any drawing activity that is currently taking place. This message is posted when the user selects the Stop drawing option from the menu bar.
 * <tt>UM_PAINT</tt> when the window procedure on the main thread receives a <tt>WM_PAINT</tt> message. This tells the worker thread to call its painting routine and paint the window background.

All drawing, including the <tt>WM_PAINT</tt> message processing, happens on the second thread. Whenever a <tt>UM_PAINT</tt> message arrives, the function <tt>DoPaint</tt> is called to repaint the window's client area Note that in some cases, especially where an application keeps one presentation space around for a long period of time and uses it for all drawing, it might be faster for the main thread to handle the paint message partially itself. In other words, issue the <tt>WinFillRect</tt> to paint the client window background immediately on the main thread before posting other paint requests on to the second thread. This prevents a time delay before the window background is repainted. For this example, to keep things relatively simple, a new PS is obtained, via <tt>WinGetPS</tt>, for each piece of drawing that takes place so this technique is not used. <PRE> //-- // // DoPaint // //-- VOID DoPaint( HWND hwnd ) { RECTL rectl; HPS  hps;

WinQueryWindowRect( hwnd, rectl ); hps = WinGetPS( hwnd ); WinFillRect( hps, rectl, CLR_BLACK ); WinReleasePS( hps ); } </PRE> Sample Code 5.

The Drawing Procedures
The function <tt>DrawShape</tt> controls the actual drawing <tt>DrawShape</tt> is called, in a loop, by <tt>ThreadProc</tt>. Based on the current user choice, <tt>DrawShape</tt>, which is really just a work dispatcher, will call one of the actual drawing routines, <tt>DrawLines, DrawBoxes, DoCube, DoMultipleCubes,</tt> or <tt>DoTextString</tt>.

The drawing routines use many of the techniques that we described in the last few issues of The Developer Connection News, especially use of the model transform to achieve some pleasing graphics effects. The <tt>DrawLines</tt> function is shown below. The source for the other drawing functions can be found on your accompanying Developer Connection for OS/2 CD-ROM <PRE> //-- // DrawLines //-- VOID DrawLines( HWND hwnd, PPOINTL ppointlStart, PRECTL prectl ) { HPS hps; POINTL pointlLine; MATRIXLF m; LONG i, xlen, ylen;

hps = WinGetPS( hwnd );

GpiSetColor( hps, rand % CLR_PALEGRAY ); GpiQueryModelTransformMatrix( hps, 9L, m );

xlen =  ( rand % prectl->xRight ) /10; ylen =  ( rand % prectl->yTop ) /10;

for ( i=0; i<360; i+=10 ) {   GpiRotate( hps, m, TRANSFORM_REPLACE, MAKEFIXED(i,0), ppointlStart ); GpiSetModelTransformMatrix( hps, 9L, m, TRANSFORM_REPLACE );

GpiSetCurrentPosition( hps, ppointlStart);

pointlLine y = ppointlStart->y + ylen ; pointlLine x = ppointlStart->x + xlen;

GpiLine( hps, pointlLine ); }

WinReleasePS( hps ); } </PRE> Sample Code 6.

Arbitration Schemes
Because the main thread stays responsive and allows the user to keep making menu choices, some form of control is needed. You might not want a user to keep making the same menu choice repeatedly, while the previous choice is still running on the second thread. There are many ways of doing this. In the Rotline program, the client window procedure disables menu items that it does not want the user to be able to select. For example, when the second thread is busy drawing, the Start drawing menu choice is disabled, but the Stop drawing menu choice is enabled. This is a good approach because it gives the user visible feedback as to what can and cannot be done at any time. It also prevents large amounts of extra code having to be written to handle nesting cases, where the same commands can get issued multiple times.

Summary
So, the key to a well-written, well-behaved PM graphics application is staying responsive to the user by use of threads and sensible inter-thread communication. In this article, we have focused on a two-threaded application that uses <tt>WinPostQueueMsg</tt> to communicate between the two threads. As your applications become more complex, and especially during certain tasks that are computer intensive, you might find the need to add yet more threads to the application. This is good practice and is encouraged.