Threads in PM Applications

From EDM2
Jump to: navigation, search

Written by Larry Salomon Jr.

Introduction

Because of what is often perceived as a design flaw in PM (but I am not passing judgement), tasks that require more time than is suggested by IBM's "well-behaved" application guideline (1/10 second) should be performed in a thread separate from that which contains the main dispatch loop (denoted by the calls to WinGetMsg() and WinDispatchMsg()). However, the issue of communication between the user-interface and additional threads created by the user-interface arises for which there is no recommended design to follow. This article will attempt to design an architecture that is easy to implement yet expandible and requires no global variables (always a good thing).

Historical Caveat

In the years that I have written applications for PM, I have tried every conceivable technique to accomplish mulithreading in a smooth fashion. For those actions that require the user to initiate the task that requires the additional thread, I have found the solution that will be detailed here to be the best for my purposes. It should be stressed here that your mileage may vary and that for your design "methodologies" this may not work as well for you. It is encouraged then to use the information herein as a basis and not as the final result.

From User-events to Multi-threading

What happens when the user selects the "Open..." menu item from your application's menu? Ignoring specifics, we can construct a timeline like the one below:

  1. The user selects "Open..." from the menu
  2. The application is notified of this selection
  3. The application prompts the user for a filename
  4. The application reads the selected file
  5. The user is then allowed to perform operations on the file's data

This is an example of a user-request that, once the relevant information is gathered, can be performed without any further intervention. Because of its simplicity, this lends itself nicely to multithreading: the thread is started, it performs its task to completion (successful or otherwise), and it notifies the user-interface that it is finished. Before multithreading can be implemented, however, some thought to design must be given.

Designing the Architecture

Because the only communication required between the owner thread and the owned thread is upon initialization and completion, we immediately eliminate the system methods of IPC, such as pipes, queues, and shared memory because they are too cumbersome. This does not mean that they will not work, but when you consider that any PM application you write that does more than draw a box or two will require many different types of threads, you will want a design that is the easiest to implement so that you can concentrate on the rest of the coding. For this reason, I chose the communication path to be a structure whose address is passed via the single thread parameter for initialization and posted messages to the owner window for completion of the thread. This treats the thread as though it were a "black-box", i.e. you put something in one end and out from the other end comes a result.

An Objective Approach

"What is Life? Who is God? What is the grass green and the sky blue? What are the common characteristics of the threads we've been discussing?" Although you could ponder the first three questions for a long time, the last one is a no-brainer given the power of hindsight. :) These commonalities I encapsulated in the THREADINFO structure, shown below:

typedef struct _THREADINFO {
   //-------------------------------------------------------
   // Initialized by the main thread
   //-------------------------------------------------------
   ULONG ulSzStruct;
   HWND hwndOwner;
   BOOL bKillThread;
   //-------------------------------------------------------
   // Initialized by the secondary thread
   //-------------------------------------------------------
   HAB habThread;
   BOOL bThreadDead;
   BOOL bResult;
} THREADINFO, *PTHREADINFO;

ulSzStruct
This specifies the size of the structure.
hwndOwner
This specifies the handle of the owning window.
bKillThread
This is set to TRUE by the owner when the request is to be aborted.
habThread
This specifies the anchor block handle of the thread. Before I start receiving hate-mail from the purists out there claiming that this isn't necessary, I answer that it is necessary and that you should keep reading to see why.
bThreadDead
This is set to TRUE by the thread when it is dead. Again, keep reading to see how this is possible.
bResult
This is a blanket indicator of success or failure complete the task.

From here we can add our task-specific variables in the following manner: for each task type, define a structure to contain the fields specifies to that task, but define the first field to always be of type THREADINFO. What does this buy us? Since the THREADINFO structure contains common (that is the key word) fields, we can write code that initializes them regardless of the task to be performed by casting the task-specific structure to type THREADINFO. Again, I realize that the purists are going to tell me that casting is a "bad thing", but I counter with the statement that casting - like goto statements - can be very helpful as long as it is not abused and is used in the proper way. If you are the sensitive type regarding these issues, stop reading now 'cause we are going to "break" a few more rules along the way.

In addition to defining a task-specific structure, we need to define a constant that uniquely describes this task. The purpose of this will be seen later. We can illustrate these things with an example.

#define ASYNC_OPEN               0x00000001L

typedef struct _OPENTHREADINFO {
   THREADINFO tiInfo;
   CHAR achFilename[CCHMAXPATH];
   PFILEDATA pfdData;      // Make-believe data type for
                           // illustrative purposes
} OPENTHREADINFO, *POPENTHREADINFO;

Add a Tire or Two

Although a tire does not provide the impulse energy to make a car go forward, without it the car is not going anywhere. Let's add a tire or two to our design. Backing up a bit, we stated that a window has a menu with a menu item that results in a thread being created to service the request associated with that menu item. Thus, the window is said to own the thread. Since conceivably a window can have the need to perform the same service in different places, it would be nice to have a single point of entry and exit for all requests. Enter "thing one" and "thing two" (for those of you who remember "The Cat in the Hat").

#define MYM_BASE                 (WM_USER)
#define MYM_STARTTHREAD          (MYM_BASE)
#define MYM_ENDTHREAD            (MYM_BASE+1)

For what are these user-messages used? MYM_STARTTHREAD is sent by the window to itself to start a thread which will perform some task. MYM_ENDTHREAD is sent by the thread to the owning window (whose handle is in the THREADINFO structure, you'll remember) to indicate that processing has completed. Both messages expect the thread type constant to be specified in LONGFROMMP(mpParm1) and a pointer to the thread-specific structure in PVOIDFROMMP(mpParm2). The skeleton code for these two messages is shown below:

typedef VOID (* _Optlink PFNREQ)(PVOID);

MRESULT EXPENTRY windowProc(HWND hwndWnd,
                            ULONG ulMsg,
                            MPARAM mpParm1,
                            MPARAM mpParm2)
{
   switch (ulMsg) {
     :
   case MYM_STARTTHREAD:
      {
         ULONG ulBit;
         PTHREADINFO ptiInput;
         PFNREQ pfnThread;
         PVOID pvParm;

         ulBit=LONGFROMMR(mpParm1);
         ptiInput=(PTHREADINFO)PVOIDFROMMP(mpParm2);

         ptiInput->hwndOwner=hwndWnd;
         ptiInput->bKillThread=FALSE;

         switch (ulBit) {
         case ASYNC_OPEN:
            {
               POPENTHREADINFO potiInfo;

               ptiInput->ulSzStruct=sizeof(OPENTHREADINFO);

               potiInfo=(POPENTHREADINFO)malloc(sizeof(OPENTHREADINFO));
               if (potiInfo==NULL) {
                  WinMessageBox(HWND_DESKTOP,
                                hwndWnd,
                                "There is not enough memory.",
                                "Error",
                                0,
                                MB_OK|MB_ICONEXCLAMATION|MB_MOVEABLE);
                  return MRFROMSHORT(FALSE);
               } /* endif */

               memcpy(potiInfo,ptiInput,sizeof(OPENTHREADINFO));
               pfnThread=(PFNREQ)openThread;
               pvParm=(PVOID)potiInfo;
            }
            break;
         default:
            WinMessageBox(HWND_DESKTOP,
                          hwndWnd,
                          "There is an internal error.",
                          "Error",
                          0,
                          MB_OK|MB_ICONEXCLAMATION|MB_MOVEABLE);
            return MRFROMSHORT(FALSE);
         } /* endswitch */

         if (_beginthread(pfnThread,NULL,0x4000,pvParm)==-1) {
            free(pvParm);
            WinMessageBox(HWND_DESKTOP,
                          hwndWnd,
                          "The thread could not be created.",
                          "Error",
                          0,
                          MB_OK|MB_ICONEXCLAMATION|MB_MOVEABLE);
            return MRFROMSHORT(FALSE);
         } /* endif */
      }
      break;
   case MYM_ENDTHREAD:
      {
         ULONG ulBit;
         PTHREADINFO ptiInput;

         ulBit=LONGFROMMR(mpParm1);
         ptiInput=(PTHREADINFO)PVOIDFROMMP(mpParm2);

         //-------------------------------------------------
         // Wait for the thread to finish dying.  There is a
         // bug in DosSleep() such that if 0 is the argument,
         // nothing happens. Call it with 1 instead to
         // achieve the same result.
         //-------------------------------------------------
         while (!ptiInput->bThreadDead) {
            DosSleep(1);
         } /* endwhile */

         switch (ulBit) {
         case ASYNC_OPEN:
            {
               POPENTHREADINFO potiInfo;

               potiInfo=(POPENTHREADINFO)ptiInput;
               free(potiInfo);
            }
            break;
         default:
            return MRFROMSHORT(FALSE);
         } /* endswitch */
      }
      break;
     :
   default:
      return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2);
   } /* endswitch */
}

Since the MYM_STARTTHREAD message can be called from different place and each place can have different values, the thread-specific values are initialized before this message is sent.

An important note is needed here: some time ago I received a note from a columnist in a printed publication criticizing me for specifying so large a stack on the call to _beginthread(). What it boiled down to was a misunderstanding of the inner workings of OS/2 with regards to stacks and actual memory usage.

The stack size specified in the .DEF file or the call to _beginthread() is allocated stack space and not committed stack space. The distinction here is important. Allocated memory is memory that has been assigned to a process but does not consume any physical memory until it is committed. How does this committment take place? Delving into a lot of system-specific stuff here that you can skip if you are not interested, the system commits a small (1?) number of pages for the stack when each thread starts, and sets the page following the stack to be a guard page. When the stack grows beyond the committed length, a guard page exception occurs, which is intercepted by the system's default exception handler. Assuming that you have more space available, the system commits enough pages to satisfy the memory requirements and sets the next page to be the new guard page.

Thus, the amount of committed memory of the stack grows (and possibly shrinks; here my knowledge is fuzzy at best) dynamically. The number specified in the .DEF file, etc. is the maximum size that the stack should grow to. If, however, you specify 32k and only use 4k, only 4k is committed.

End of sermon.

Next Add the Engine

The thread is defined like you would expect it to, but it performs a few important tasks.

VOID openThread(POPENTHREADINFO potiInfo)
{
   HAB habAnchor;
   HMQ hmqQueue;

   habAnchor=WinInitialize(0);
   hmqQueue=WinCreateMsgQueue(habAnchor,0);
   WinCancelShutdown(hmqQueue,TRUE);

   potiInfo->tiInfo.habThread=habAnchor;
   potiInfo->tiInfo.bThreadDead=FALSE;
   potiInfo->tiInfo.bResult=FALSE;

   //-------------------------------------------------------
   // Do nothing. This is strictly for the purposes
   // of illustration.
   //-------------------------------------------------------
   WinMessageBox(HWND_DESKTOP,
                 potiInfo->tiInfo.hwndOwner,
                 "In the thread",
                 "Information",
                 0,
                 MB_OK|MB_MOVEABLE);

   WinPostMsg(potiInfo->tiInfo.hwndOwner,
              MYM_ENDTHREAD,
              MPFROMLONG(ASYNC_OPEN),
              MPFROMP(potiInfo));

   WinDestroyMsgQueue(hmqQueue);
   WinTerminate(habAnchor);

   DosEnterCritSec();
   potiInfo->tiInfo.bThreadDead=TRUE;
}

Initialization

The thread initializes itself by creating a message queue and initializing the remainder of the common fields in the THREADINFO structure. Why do we need a message queue? I claim it is by nature of the fact that this thread exists because of a user action, you will likely call a function that requires it (e.g. WinMessageBox() in the above code).

Termination

The thread terminates by posting a message to the owner window that it is about to complete, destroys the message queues, and - what's that? - enters a critical section to set the bThreadDead flag. Why do we need that?

We don't. The reason for this is purely historical, as is the posting (versus sending) of the MYM_ENDTHREAD message. However, this illustrates a point that has confused people in the past: if a thread enters a critical section and then dies, the system automatically marks the critical section as having been exited. Thus, all suspended threads will resume execution.

Although critical sections are a no-no when writing PM applications (because the user-interface thread is also halted), this critical section takes little time to execute, so we can let it be.

Finally Add the Rollbars and Racing Stripes

User feedback becomes a big issue when you deal with multiple threads. How do you indicate that something important is going on in the background? Do you disable the (appropriate) menu items? How are errors handled? etc. The easiest answer is "it depends on the application". I realize that this does not help much, so we will now discuss techniques that can be applied in your applications.

Mouse Pointers

The most common method of indicating that something else is going on is to change the mouse pointer. However, we all know what happens when you do this regardless of the mouse position. The answer, then, lies in two important messages that the system sends regarding the mouse - WM_CONTROLPOINTER and WM_MOUSEMOVE. Simply stated, when you receive those mouse messages, you set the mouse to an appropriate pointer (typically WinQuerySysPointer(HWND_DESKTOP,SPTR_WAIT,FALSE)) if another thread is in progress or you return WinDef*Proc() otherwise. How do you tell if there is another thread? There is no documented system API that I know of that will tell you this, so you will have to keep a counter in your instance data and increment/decrement as necessary in the MYM_STARTTHREAD/MYM_ENDTHREAD messages.

Menu Items

Whenever a menu is activated, the system sends the client window a WM_INITMENU message. By intercepting and "switch"-ing on the value of mpParm1 (which is the identifier of the menu), you can enable or disable any items of interest.

Error Messages

Error messages, in my opinion, are best handled in the thread which detected the error. You could, of course, add a numeric field in the THREADINFO structure in which you place an application-defined error code and then check that in the MYM_ENDTHREAD processing, but why clutter it up unnecessarily? If you'll accept that, then the next question is what do you do when you encounter an error in the thread? The strategy I follow is like this:

  • In the initialization of the thread, set all variables which specify resources to be allocated by the thread (bitmap handles, file handles, memory pointers, etc.) to the equivalent of "unallocated". Also, set the bResult field of the THREADINFO structure to FALSE.
  • Prefix the termination section of the thread with a label (i.e. EXIT_PROC). Just before the label, set the bResult field of the THREADINFO structure to TRUE because if you have made it there, everything must have completed successfully.
  • If an error is encountered, display an error message (via WinMessageBox()) and execute goto EXIT_PROC.

Notes of interest:

  1. You cannot name the label EXIT_THREAD, because that is a constant already defined by the toolkit for the DosExit() API. Although this seems obvious, unless you're already thinking about it, you will spend hours on a wild goose chase through your own code looking for the "duplicate symbol defined" error (like I did once).
  2. Don't forget to check the resource handles to see if they are allocated before you try to unallocate them. This is the purpose of the first step in the list above.

A complete working sample application demonstrating this asynchronous design has been provided in the async.zip file.

Synchronicity

As an advanced topic, another approach to this mess is to process the request in a synchronous fashion. What? Yes, you read it correctly. What you need is another way of looking at synchronicity.

Whoooooa Whoooooa Whooooa!

(I don't know how many Police fans there are out there.) Synchronicity in the above paragraph is, as Einstein would have stated it, based on frame of reference. What if we were somehow able to make the processing look like a simple function call to the user-interface thread, but in actuality process it asynchronously?

Before we can do this, we have to determine what is different between a simple function call and what we are trying to accomplish here. The answer is that we must remain responsive to the user-interface messages. This requires a little "hocus-pocus".

Going back to "PM Programming Course 101", you'll remember that the system changes focus by sending a bunch of messages to the windows losing and receiving the focus. If either window does not respond to these messages within a system-defined amount of time, you get the "The application is not responding to system messages..." message. The ultimate question then becomes "how do you process messages?" and the answer is trivial, my dear reader. Simply go into a WinPeekMsg()/ WinDispatchMsg() loop.

Of course, things are a little more than that, so the code is presented below. A complete working sample application has been provided in the sync.zip file.

#define DT_NOERROR               0
#define DT_QUITRECEIVED          1
#define DT_ERROR                 2

typedef VOID (* _Optlink PFNREQ)(PVOID);

USHORT dispatchThread(HAB habAnchor,
                      PFNREQ pfnThread,
                      PTHREADINFO ptiInfo)
//----------------------------------------------------------
// This is the thread dispatch procedure.  It calls
// _beginthread() and goes into a WinPeekMsg()/WinDispatchMsg()
// loop until the thread is

// finished or WM_QUIT is received.  Note the semantics of
// the latter event:

// if WM_QUIT is received, then it is assumed that the
// application will kill itself on return and thus any system
// resources will automatically be unallocated by the system
// when the application ends. So we do not set bKillThread=TRUE
// and wait but instead call DosKillThread() and return.
//----------------------------------------------------------
{
   TID tidThread;
   BOOL bLoop;
   QMSG qmMsg;

   ptiInfo->bKillThread=FALSE;
   ptiInfo->bThreadDead=FALSE;

   tidThread=_beginthread(pfnThread,NULL,0x4000,ptiInfo);
   if (tidThread==-1) {
      return DT_ERROR;
   } /* endif */

   //-------------------------------------------------------
   // WinGetMsg() cannot be used because it blocks if there
   // is no message waiting.  When the thread dies, therefore,
   // the function will never return if the user takes his/her
   // hands off of the keyboard, mouse, and no timers are
   // started because we will never get a message!
   //-------------------------------------------------------
   WinPeekMsg(habAnchor,&qmMsg,NULLHANDLE,0,0,PM_REMOVE);
   bLoop=((qmMsg.msg!=WM_QUIT) && (!ptiInfo->bThreadDead));

   while (bLoop) {
      WinDispatchMsg(habAnchor,&qmMsg);
      WinPeekMsg(habAnchor,&qmMsg,NULLHANDLE,0,0,PM_REMOVE);
      bLoop=((qmMsg.msg!=WM_QUIT) && (!ptiInfo->bThreadDead));
   } /* endwhile */

   if (qmMsg.msg==WM_QUIT) {
      DosKillThread(tidThread);
      return DT_QUITRECEIVED;
   } /* endif */

   return DT_NOERROR;
}

Summary

In this article, much information was presented regarding multithreading and PM application development. Even though our scope was limited to "one-shot" threads, we can see how a complicated matter can be simplified with a little thought. This is not to belittle the task of such a complex objective, but instead is to illustrate the ability to implement an architecture that is usable in the "real-world".

A summary follows:

  • All "one-shot" threads share a common set of data fields.
  • Exploitation of the "black-box" concept simplifies things considerably.
  • Since threads of the same type can be created from different places, a single-entry and exit point is desirable.
  • User-feedback issues cannot be ignored.
  • "One-shot" threads can be implemented with the owner having a synchronous perspective.

All questions are welcome via email.