Multithreading Presentation Manager Applications

From EDM2
Jump to: navigation, search

By Monte Copeland

Download Source Code Files.

Over the years, many different styles have emerged to multithread Presentation Manager (PM) applications.

The approach presented in this article is the start-a-thread, keep-it-around, and give-it-work approach and is based on PM message queues. It is a good choice, because PM applications must have message queues already. Additionally, this choice lets you achieve multithreading without using semaphores.

Why Multithreaded PM Applications Are a Must

PM applications, like all OS/2 applications, call into the operating system for services using the OS/2 APIs. However, PM applications must provide functions, called window procedures, for PM to call.

PM delivers messages to a PM application by calling window procedures. For example, these messages include menu selections, mouse clicks, and termination-notification messages. In addition, PM sends a message to the application when it is time to repaint the window or it can deliver user-defined messages.

For PM to stay synchronized with all the applications on the desktop, it delivers messages one at a time. These are sent messages. In other words, when PM sends a message to your window procedure, it stops sending messages elsewhere until your procedure returns.

Therefore, PM applications must respond to all sent messages in a timely manner. The often-cited response time is 1/10th second; hence, the 1/10th-second rule. When an application takes too long to respond to messages, unacceptable behavior can occur.

This article describes a robust, two-threaded PM application architecture. Applications coded in this style obey the 1/10th second rule, and yet, can perform long tasks. A second thread in PM applications is created to bust the 1/10th-second rule, and perform long tasks, such as diskcopy, upload/download, file input/output, or SQL queries.

Thread Responsibilities

Thread 1 is responsible for presentation. It operates the first message queue that the application creates. This message queue gets and dispatches messages to one or more application windows on the desktop.

Thread 1 presents the client window, as well as the dialog and message boxes. It reacts to command messages from menus and child controls. It processes messages that the frame-window controls generate. In fact, thread 1 is devoted to the operation of all the application's visible windows on the desktop.

Thread 2 operates a second message queue that is created by thread 2 of the application. This message queue delivers messages to an object window and its window procedure. Object windows are invisible, that is, they do not appear on the desktop. More importantly, they are not bound by the 1/10th-second rule.

When object-window procedures run on thread 2, they are perfect for doing any time-consuming tasks required by the application. While the object window is busy working on a task, the main-window procedure continues to get and dispatch messages in a timely manner-as it must.

Post a Message, Do Some Work

Thread 1 creates thread 2, and thread 2 creates its own message queue for its object window. Then it blocks in the call to the API, WinGetMsg, in the message loop. (See the OBJECT.C file in the sample code that follows this article.)

The object window stays blocked until there is work to do. For example, a user selects a task from a pull-down menu. PM sends a menu message to the client window procedure on thread 1. Thread 1 calls the API, WinPostMsg, and posts a user-defined messa ge to the object window on thread 2. Thread 2 performs the task in the object-window procedure. When the task is complete, thread two posts an acknowledgement back to the originating window.

client/dialog                         object window
window on thread 1                    on thread 2
---------------                       ---------------      |
|             |                       | waiting in  |      |
| user        | WinPostMsg(           | WinGetMsg   |      |
| requests    |     hwndObject,       |             |      |
| a lengthy   |     WM_USER_WORKITEM, |             |      |
| workitem    |     hwndToAck,        |             |      |
|             |     null )            |             |      |
|             | --------------------> |             |
| window      |                       | perform     |     time
| disabled    |                       | lengthy     |      |
| while       |                       | task        |      |
| busy        |                       |             |      |
|             | WinPostMsg(           |             |      |
|             |    hwndToAck,         |             |      |
|             |    WM_USER_ACK,       |             |      |
|             |    WM_USER_WORKITEM,  | task        |      |
|             |    result code )      | complete    |      |
|             | <-------------------- |             |      |
| enable      |                       |             |      |
| again       |                       |             |      |
---------------                       ---------------      |
                                                           |
                                                          \ /
                                                           .

The following convention exists with the two message parameters of the API, WinPostMsg, when posting a user-defined work message to the window:

Message parameter one is the window handle of the originating window.

By having the window handle of the originator, the object window knows which window to acknowledge when the task completes.

The sample file, APP.C, only shows the client-window procedure originating tasks; however, dialog boxes also can originate tasks.

Disable While Busy

In the sample code, the application disables its client and selected menu item, and then posts a message to the object window to perform the lengthy task. These actions prevent the user from initiating another work item, while the object window is busy. (See the message WM_USER_DISABLE_CLIENT in the file, APP.C, which is sent before the application posts the work request to the object window.)

Note: When the application disables the client window, its message queue does not stop working.

Click on the client window, while the object window is busy. The beep you hear is proof that the client window's message queue continues to process messages.

The frame window is not disabled; therefore, the user can minimize the frame window or switch to another application while the application is busy.

The mouse pointer changes to an hourglass when it passes over the window of the busy application. When the mouse pointer leaves the application window, it changes back to a normal pointer. The application keeps a busy flag and references it during proce ssing of WM_MOUSEMOVE in the client-window procedure.

This approach is simplistic (if not rigid), but it is one that you can modify by choosing which items are grayed on which menus. If a given thread/object window pair (there could be many) was responsible for tasks A and B, you would gray items A and B, while the object window was busy working on either A or B.

Object Window Acknowledges Completion

When the lengthy task completes, the object window posts a user-acknowledgment message to the window that originated the task. This message informs the originating window that the task is complete, and that it can re-enable itself and any menu items, as required.

Use the following convention with message parameters on the acknowledgment message:

Message parameter one is the user-defined message posted to the object window.

With this parameter, the originating window can discern which activity is now complete; the second message parameter is a result code.

Closing Considerations

The act of closing the application is like a chain reaction between the two threads. Usually, client windows post a WM_QUIT message to themselves when they receive a WM_CLOSE message-not in multithreaded PM applications.

When the client window receives a WM_CLOSE message, it posts a WM_QUIT message to the object window, then it returns. When the object window receives the WM_QUIT message, it leaves its message loop, cleans up, posts a WM_QUIT message to thread 1. Thread 1 leaves its message loop, waits for thread 2 to exit, and then exits itself (and the process).

The object-window thread calls the API, WinCancelShutdown. If the user shuts down the system while the application is still running, this API tells PM not to send a WM_CLOSE message to the object-window message queue. PM sends a WM_CLOSE message to the client-window message queue, then the chain reaction starts.

Common Data

Both threads share a common data space. This space is defined by the GLOBALS structure in the app.h file. WM_CREATE processing allocates this space and passes the pointer to thread 2 on the call to _beginthread. The client- and object-window procedures keep a pointer to this memory in their window words. The number of extra words for each window is set by the API, WinRegisterClass. Both the client and object windows have 4 extra bytes of window words.

When dialog-box procedures initialize, they must obtain the pointer to shared memory, and store it in their window words. By default, dialog boxes have enough window words to hold a 32-bits-long pointer.

pmassert

The pmassert macro is a debugging tool. It works much like the C-language assert macro. Anywhere in the source code, you can assert that a Boolean expression is true. At runtime, nothing happens if the expression is true. If the expression is false, the macro displays the failed assertion in a message box along with the line number and the C source file name where the assertion failed.

Because pmassert is a macro, it is easy to redefine it to be a no operation, after you debug the application. In the C-language tradition, you accomplish this by defining the symbol, NDEBUG. This approach to program building produces both debug and ret ail versions of your program. See the pmassert.h file in the sample code.

Acknowledgments

By now you might have recognized this architecture from the OS/2 Toolkit Print Sample (PRTSAMP.EXE) on the CD-ROM. It is the same.

Sample Code

Use the following sample code to create your own multithreaded PM applications.

// start of file app.h -------------------------------------------------------

// strings
#define APP_TITLE         "Object Window Application"
#define APP_CLASS_CLIENT  "APPClient"
#define APP_CLASS_OBJECT  "APPObject"

// identifiers
#define ID_APP            3
#define IDM_SLEEP         303
#define IDM_ACTIONS       304

// lengths
#define LEN_WORKSTRING    256
#define LEN_STACK         18000

// structure to hold globals variables common to both threads
struct _globals {
 BOOL           fBusy;
 HAB            hab;
 HWND           hwndClient;
 HWND           hwndFrame;
 HWND           hwndTitlebar;
 HWND           hwndMenubar;
 HWND           hwndObject;
 TID            tidObject;
};
typedef struct _globals GLOBALS, *PGLOBALS;

// user-defined messages for work items and acknowlegements
#define WM_USER_ACK                      (WM_USER+0)
#define WM_USER_SLEEP                    (WM_USER+1)
#define WM_USER_ENABLE                   (WM_USER+2)
#define WM_USER_DISABLE                  (WM_USER+3)

// function prototypes -- _Optlink is a IBM C SET/2 modifier
void _Optlink threadmain( void *  );
PGLOBALS Create( HWND hwnd );
MRESULT EXPENTRY ObjectWinProc( HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2 );
MRESULT EXPENTRY ClientWinProc( HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2 );

// end of file app.h ----------------------------------------------------------

//-------------------------------------------------------------------
// pmassert.h
#ifndef NDEBUG
#define pmassert(hab,exp)\
{\
if(!(exp)) {\
 char ebuff[ 64 ]; unsigned long errorid; unsigned short shortrc;\
 errorid = WinGetLastError( hab ); \
 sprintf( ebuff, "Line %d\nFile %s\nLast Error %p\nExpression %s\n",\
                __LINE__, __FILE__, errorid, #exp );\
 shortrc = WinMessageBox( HWND_DESKTOP, HWND_DESKTOP, ebuff,\
                "Assertion failed. Continue?", 0, MB_YESNO  );\
 if( shortrc == MBID_NO ) exit( 1 );\
 }\
}
#else
 #define pmassert(hab,exp)
#endif
// end of file pmassert.h

// start of file app.c -------------------------------------------------------
// A sample PM application showing use of an object window operated by
// thread 2 for time-consuming tasks.

// os2 includes
#define INCL_DOSPROCESS
#define INCL_WIN
#include <os2.h>
// c includes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
// app includes
#include "app.h"
#include "pmassert.h"

// ----------------------------------------------------------------------
// main entry point for thread 1
int main ( void )
{
 APIRET   rc;
 BOOL     fSuccess;
 HAB      hab;
 HMQ      hmq;
 HWND     hwndClient;
 HWND     hwndFrame;
 QMSG     qmsg;
 ULONG    flCreate;
 PGLOBALS pg;

 // PM application init
 hab = WinInitialize( 0 );
 hmq = WinCreateMsgQueue( hab, 0 );
 assert( hmq );

 // register client window class
 // with 4 bytes of window words to hold a pointer to globals
 fSuccess = WinRegisterClass( hab, APP_CLASS_CLIENT, (PFNWP)ClientWinProc,
                     CS_SIZEREDRAW | CS_CLIPCHILDREN, sizeof( PGLOBALS ) );
 pmassert( hab, fSuccess );

 flCreate = FCF_SYSMENU | FCF_SIZEBORDER    | FCF_TITLEBAR |
            FCF_MINMAX  | FCF_SHELLPOSITION | FCF_TASKLIST |
            FCF_MENU    | FCF_ICON;

 // standard window create; returns after WM_CREATE processing finishes
 hwndFrame = WinCreateStdWindow( HWND_DESKTOP, WS_VISIBLE, &flCreate,
              APP_CLASS_CLIENT, APP_TITLE, 0, 0, ID_APP, &hwndClient );
 pmassert( hab, hwndFrame );
 pmassert( hab, hwndClient );
 pg = (PGLOBALS) WinQueryWindowULong( hwndClient, QWL_USER );

 // dispatch user input messages
 while( WinGetMsg( hab, &qmsg, 0, 0, 0 ))
 {
   WinDispatchMsg( hab, &qmsg );
 }

 // wrap up
 WinDestroyWindow ( hwndFrame );
 WinDestroyMsgQueue ( hmq );
 WinTerminate ( hab );

 rc = DosWaitThread( &pg->tidObject, DCWW_WAIT );
 assert( rc == 0 );

 // exit the process
 return 0;
}

// ----------------------------------------------------------------------
// client window procedure
MRESULT EXPENTRY ClientWinProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2 )
{
 HAB           hab;
 HPS           hps;
 PGLOBALS      pg;
 RECTL         rectl;
 ULONG         ulWork;

 switch( msg ) {
 case WM_CLOSE:
   // get pointer to globals from window words
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   // tell object window to quit, then exit its thread
   WinPostMsg( pg->hwndObject, WM_QUIT, 0, 0 );
   return (MRESULT) 0;

 case WM_COMMAND:
   // get pointer to globals from window words
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   switch( SHORT1FROMMP( mp1 )) {
   case IDM_SLEEP:
     // disable client for this lengthy task
     WinSendMsg( hwnd, WM_USER_DISABLE, 0L, 0L );
     // object window is busy now
     pg->fBusy = TRUE;
     // tell object window to perform lengthy task
     WinPostMsg( pg->hwndObject, WM_USER_SLEEP, (MPARAM)hwnd, 0 );
     // wait for ack
     break;
   }
   return (MRESULT) 0;

 case WM_CREATE:
   hab = WinQueryAnchorBlock( hwnd );
   // allocate memory for global variables; see GLOBALS struct in app.h
   pg = (PGLOBALS) malloc( sizeof( GLOBALS ));
   pmassert( hab, pg );
   // initialize globals to zero
   memset( pg, 0, sizeof( GLOBALS ));
   // store globals pointer into client window words; see WinRegisterClass
   WinSetWindowULong( hwnd, QWL_USER, (ULONG) pg );
   // disable until object window initializes
   WinSendMsg( hwnd, WM_USER_DISABLE, 0, 0 );
   // initialize globals with important data
   pg->hab          = hab;
   pg->hwndClient   = hwnd;
   pg->hwndFrame    = WinQueryWindow( hwnd, QW_PARENT );
   pg->hwndTitlebar = WinWindowFromID( pg->hwndFrame, FID_TITLEBAR );
   pg->hwndMenubar  = WinWindowFromID( pg->hwndFrame, FID_MENU );
   // create thread 2 for object window; pass pointer to globals
   pg->tidObject = _beginthread( threadmain, NULL, LEN_STACK, (void *)pg );
   pmassert( hab, pg->tidObject );
   return (MRESULT) 0;

 case WM_MOUSEMOVE:
   // if busy, display the wait pointer, else the arrow pointer
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   ulWork = pg->fBusy ? SPTR_WAIT : SPTR_ARROW;
   WinSetPointer(HWND_DESKTOP,WinQuerySysPointer(HWND_DESKTOP,ulWork,FALSE));
   return (MRESULT) TRUE;

 case WM_PAINT:
   hps = WinBeginPaint( hwnd, 0, &rectl );
   WinFillRect( hps, &rectl, SYSCLR_WINDOW );
   WinEndPaint( hps );
   return (MRESULT) 0;

 case WM_USER_ACK:
   // get pointer to globals from window words
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   // object window has completed which task?
   switch( (ULONG) mp1 ) {
   case WM_USER_SLEEP:
     WinMessageBox(HWND_DESKTOP,pg->hwndFrame,"Done.",APP_TITLE,0,MB_CANCEL);
     break;
   }
   WinSendMsg( hwnd, WM_USER_ENABLE, 0, 0 );
   // object window is not busy anymore
   pg->fBusy = FALSE;
   return (MRESULT) 0;

 case WM_USER_DISABLE:
   // get pointer to globals from window words
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   // this message sent by client
   WinEnableWindow( pg->hwndClient, FALSE );
   // this is a macro defined in pmwin.h
   WinEnableMenuItem( pg->hwndMenubar, IDM_SLEEP, FALSE );
   return (MRESULT) 0;

 case WM_USER_ENABLE:
   // get pointer to globals from window words
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   // this message sent by client
   WinEnableWindow( pg->hwndClient, TRUE );
   // this is a macro defined in pmwin.h
   WinEnableMenuItem( pg->hwndMenubar, IDM_SLEEP, TRUE  );
   return (MRESULT) 0;
 }
 // default
 return WinDefWindowProc( hwnd, msg, mp1, mp2 );
}
// end of file app.c ---------------------------------------------------------
// start of file object.c ---------------------------------------------------
// the object window thread and procedure on thread 2

// os2 includes
#define INCL_DOSPROCESS
#define INCL_WIN
#include <os2.h>
// crt includes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// app includes
#include "app.h"
#include "pmassert.h"

//----------------------------------------------------------------------
// thread 2 entry point: gets and dispatches object window messages
// _Optlink is an IBM C Set/2 function modifier
void _Optlink threadmain( void * pv  )
{
 BOOL       fSuccess;
 HAB        hab;
 HMQ        hmq;
 PGLOBALS   pg;
 QMSG       qmsg;

 // cast and set pointer to globals
 pg = (PGLOBALS) pv;

 // thread initialization
 hab = WinInitialize( 0 );
 hmq = WinCreateMsgQueue( hab, 0 );

 // prevent system from posting object window a WM_QUIT
 // I'll post WM_QUIT when it's time.
 fSuccess = WinCancelShutdown( hmq, TRUE );
 pmassert( hab, fSuccess );

 fSuccess = WinRegisterClass( hab, APP_CLASS_OBJECT,
                 (PFNWP)ObjectWinProc, 0, sizeof( PGLOBALS ));
 pmassert( hab, fSuccess );

 pg->hwndObject = WinCreateWindow( HWND_OBJECT, APP_CLASS_OBJECT, "",
            0, 0, 0, 0, 0, HWND_OBJECT, HWND_BOTTOM, 0, (PVOID)pg, NULL );
 pmassert( hab, pg->hwndObject );

 // created OK, ack client
 WinPostMsg( pg->hwndClient, WM_USER_ACK, 0, 0 );

 // get/dispatch messages; user messages, for the most part
 while( WinGetMsg ( hab, &qmsg, 0, 0, 0 ))
 {
   WinDispatchMsg ( hab, &qmsg );
 }

 // tell client window to quit
 WinPostMsg( pg->hwndClient, WM_QUIT, 0, 0 );

 // clean up
 WinDestroyWindow( pg->hwndObject );
 WinDestroyMsgQueue( hmq );
 WinTerminate( hab );
 return;
}

// --------------------------------------------------------------------------
// object window procedure; mp1 is the window to acknowledge upon completion
MRESULT EXPENTRY ObjectWinProc( HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
{
 PGLOBALS      pg;
 HWND          hwndToAck;

 // store the handle of the window to ack upon task completion;
 hwndToAck = (HWND)mp1;

 switch( msg ) {
 case WM_CREATE:
   // for the create case, mp1 is pointer to globals;
   // save it in object window words; dependency on WinRegisterClass
   pg = (PGLOBALS) mp1;
   WinSetWindowULong( hwnd, QWL_USER, (ULONG) pg  );
   return (MRESULT) 0;

 case WM_USER_SLEEP:
   // get pointer to globals from window words
   pg = (PGLOBALS) WinQueryWindowULong( hwnd, QWL_USER );
   // sleep as though this were a time-consuming task
   DosSleep( 20000 );
   DosBeep( 500, 150 );
   // tell originating window that the task is complete
   WinPostMsg( hwndToAck, WM_USER_ACK, (MPARAM) msg, 0 );
   return (MRESULT) 0;
 }

 // default:
 return WinDefWindowProc( hwnd, msg, mp1, mp2 );
}
// end of file object.c ------------------------------------------------------

/* start of file app.rc ----------------------------------------------*/

#include "os2.h"
#include "app.h"

POINTER ID_APP THREADS.ICO

MENU ID_APP
BEGIN
 SUBMENU    "Actions",                 IDM_ACTIONS,            MIS_TEXT
 BEGIN
   MENUITEM "Sleep & Beep",            IDM_SLEEP,              MIS_TEXT
 END
END

/* end of file app.rc -------------------------------------------------*/

; ---------------------------------------------------------------------
; app.def

NAME app WINDOWAPI

STUB    'OS2STUB.EXE'

DATA    MULTIPLE

STACKSIZE  12192
HEAPSIZE    8200

PROTMODE
EXPORTS
 OBJECTWINPROC
 CLIENTWINPROC

; end of file app.def -------------------------------------------------

#-------------------------------------------------------------------
# makefile for APP.EXE, the 2-threaded PM sample

CC   = icc /c /gd- /re /ss /ms /gm+ /ti+ /q+ /Sm /kb+
LINK = link386 /nod /cod /map
LIBS = dde4mbs + os2386
H    = app.h
OBJ  = app.obj object.obj

all: app.exe

app.exe: $(OBJ) app.res app.def
 $(LINK)  $(OBJ) ,,, $(LIBS) , $*
 rc app.res

app.res: $*.rc threads.ico app.h
 rc -r $*.rc

app.obj: $*.c $(H)
 $(CC) $*.c

object.obj: $*.c $(H)
 $(CC) $*.c

# end of file makefile ---------------------------------------------

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