Writing Multithreaded Graphics Programs

From EDM2
Jump to: navigation, search

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. Having done this, it calls the WinCancelShutdown function. The purpose of WinCancelShutdown is to tell PM not to post a WM_QUIT 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 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 and did not process their message queues. All subsequent releases of the operating system post the WM_QUIT message to the main thread first. It is, however, very good practice for all PM worker threads that have message queues to call WinCancelShutdown, 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 );
}

Sample Code 2. ThreadProc function

The Main Thread Window Procedure

The client window procedure for the main thread, ClientWndProc, is small because all it must do is either:

  • Receive events, and as appropriate, call WinDefWindowProc 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 

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 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.

#define UM_AUTO   WM_USER
#define UM_EXIT   WM_USER+1
#define UM_STOP   WM_USER+2
#define UM_PAINT  WM_USER+3

Sample Code 4.

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

The application posts the UM_AUTO message to tell the thread to start drawing the currently selected graphical shape or shapes. Upon receipt of a UM_AUTO 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 WinPeekMsg to see if a UM_EXIT, UM_STOP, or UM_PAINT 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:

  • UM_EXIT when the program is about to exit This gives the worker thread the chance to shutdown cleanly before the program ends.
  • UM_STOP 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.
  • UM_PAINT when the window procedure on the main thread receives a WM_PAINT message. This tells the worker thread to call its painting routine and paint the window background.

All drawing, including the WM_PAINT message processing, happens on the second thread. Whenever a UM_PAINT message arrives, the function DoPaint 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 WinFillRect 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 WinGetPS, for each piece of drawing that takes place so this technique is not used.

//--------------------------------------------------------------------------
//
//  DoPaint()
//
//--------------------------------------------------------------------------
VOID DoPaint( HWND hwnd )
{
  RECTL rectl;
  HPS   hps;

  WinQueryWindowRect( hwnd,  rectl );
  hps = WinGetPS( hwnd );
  WinFillRect( hps,  rectl, CLR_BLACK );
  WinReleasePS( hps );
}

Sample Code 5.

The Drawing Procedures

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

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 DrawLines function is shown below. The source for the other drawing functions can be found on your accompanying Developer Connection for OS/2 CD-ROM

//--------------------------------------------------------------------------
//  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 );
}

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 WinPostQueueMsg 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.

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation