Multithreading Presentation Manager Applications
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.
Contents
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 message 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 | | | | --------------- --------------- | V
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 processing 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