Introduction and a bare-bones PM application (Processes and Threads)

From EDM2
Revision as of 18:38, 17 December 2016 by Ak120 (Talk | contribs)

Jump to: navigation, search

Written by Gavin Baker

Introduction

Welcome to the second installment of our exciting series on PM programming! (Well, I thought I'd better start on a high note ...)

In this article, I present a simple program which takes advantage of OS/2 threads. It uses one thread to handle interacting with the user, and another thread to do all the work. It simply displays the current time in the middle of the window. It is obviously a trivial prorgam, but nonetheless serves to illustrate one possible use of threads.

Basically you can have one program (process) doing more than one thing (threads) at once. A little like multi-multitasking... Anyway, in terms of the source code, a thread is just a function in your program which gets called in a special way which allows it to go off and run at the same time as the rest of the program. You can imagine how complicated things could get having to co-ordinate things, so OS/2 also provides IPC (Inter-Process Communication) functions to help you.

The program uses other facets of OS/2 not discussed yet in this series (resources, and the Graphics Programming Interface [GPI]) so we will not spend too much time on them since the main focus is on threads. I have included the RC file which defines the dialog box and the menu, but I will leave off explaining resources to the next article.

Scope

I am assuming that you are a competent C programmer, and have a working knowledge of OS/2 from a user's perspective. The sample code here was produced with Borland C++ for OS/2, but should work with most other compilers.

Trademarks etc.

Please note that any trademarks referred to in this article remain the property of their respective companies.

#include <std_disclaimer.h>

Processes & Threads

A process in OS/2 terms is just an application or program. Because OS/2 is multi-tasking, you can run multiple processes (in their own address spaces) at the same time. A thread is an execution unit within a process. It becomes clearer in this diagram:

Editor's note: Due to time constraints, this diagram was not available in time for publication. It will be made available at a later date.

The red shows OS/2 itself. The green represents our program, and the yellow boxes are the threads within it. You would typically have one thread to handle the user interface (the main window procedure), one to do all the work, and perhaps one for printing. Threads can have different priorities, and there are some very powerful functions to enable threads to communicate with each other, to synchronise and co-ordinate events.

Threads within a process share the address space, which means the variables and functions within the program have the same scope between two threads. For example, consider these two threads:

 void Thread1()      |    void Thread2()
 {                   |    {
   while (1)         |      while (1)
     i++;            |        i--;
 }                   |    }

Get the feeling we may not be getting anywhere...? These two threads are playing a "tug-of-war" with the poor old (overused) variable i. The first thread will be forever incrementing i, and the second thread will forever be undoing all that hard work. The variable i will stay around the same value, but won't be exactly zero due to the dynamic prioritising of threads that OS/2 performs - if a Thread1 gets a tiny bit more CPU time, i will go up a little.

Threads allow your programs to be much more efficient, and responsive to the user. Here I will show you how.

The Code

Right - now we'll go straight to the code. The general idea is to have the main program handle the user interactions, and during initialization we start the worker thread which carries on its merry way despite what the main thread may be doing.

The way we acheive this is by creating the main window itself, and then have the second Worker thread create a dummy window (called an OBJECT WINDOW) which doesn't actually appear on screen, but gives us a window handle and a message queue to work with. This is not the only way to do this, but it is probably the simplest. The main window sends custom messages to the second dummy window on thread 2 to do all the work.

Now here we specify which portions of the include files we need, and also include a few others.

 #define INCL_WIN
 #define INCL_GPI
 #define INCL_WINDIALOGS
 
 #include <os2.h>
 #include <process.h>
 #include <string.h>
 #include <time.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <dos.h>
 #include "step02.h"

Now we need to pass messages between the two threads, so we define our own by starting at WM_USER and going up. This ensures we don't accidentally use a system-defined message.

 #define WM_BEGIN_PAINT  WM_USER+1
 #define WM_END_PAINT    WM_USER+2
 #define WM_ACK          WM_USER+3

Using global variables is generally considered a no-no, unless it can't be avoided. It makes things easier to implement and more self-contained if you keep globals to a bare minimum. We then supply prototypes for our functions.

 HWND    hwndMain,
         hwndWorker;
 
 MRESULT EXPENTRY MainWndProc (HWND, ULONG, MPARAM, MPARAM);
 VOID             WorkerThread ();
 MRESULT EXPENTRY WorkWndProc (HWND, ULONG, MPARAM, MPARAM);
 MRESULT EXPENTRY DlgProc(HWND, ULONG, MPARAM, MPARAM);
 VOID             WorkPaint (HWND, HPS);

The main function is not very big - all it does is set up some variables, the flags for the window, initialize things for PM, then register and create our window. After we exit our main message loop (by getting a WM_QUIT) we clean up and exit.

 int main (void)
 {
     HAB     hab;
     HMQ     hmq;
     HWND    hwndFrame;
     QMSG    qmsg;
     ULONG   flFrameFlags =  FCF_TITLEBAR      |
                             FCF_SYSMENU       |
                             FCF_SIZEBORDER    |
                             FCF_MINMAX        |
                             FCF_SHELLPOSITION |
                             FCF_TASKLIST      |
                             FCF_MENU;
 
     randomize();
 
     hab = WinInitialize (0);
     hmq = WinCreateMsgQueue (hab, 0);
 
     WinRegisterClass (hab, "STEP2",
                       MainWndProc, CS_SIZEREDRAW, 0);
 
     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                                     WS_VISIBLE,
                                     &flFrameFlags,
                                     "STEP2",
                                     NULL,
                                     0,
                                     NULLHANDLE,
                                     ID_MAIN,
                                     &hwndMain);
 
     while (WinGetMsg (hab, &qmsg, 0, 0, 0))
             WinDispatchMsg (hab, &qmsg);
 
     WinDestroyWindow (hwndFrame);
     WinDestroyMsgQueue (hmq);
     WinTerminate (hab);
     return 0;
 }

Now here is the Window Procedure for the main window itself. The special bit to notice here is the call to _beginthread when we get created (WM_CREATE). This is where the second thread gets started. Note we just pass it the name of the function, and that function will start executing from there all by itself. It can operate more or less like any other function, with a few considerations.

 MRESULT EXPENTRY MainWndProc (HWND hwnd,
                               ULONG msg,
                               MPARAM mp1,
                               MPARAM mp2) {
     FILEDLG     fild;
 
     switch (msg)
     {
         case WM_CREATE:
             if (_beginthread (WorkerThread,
                               8192,
                               NULL) == -1)
             {
                 WinMessageBox (HWND_DESKTOP, hwnd,
                     "Creation of second thread failed!",
                     "Step 2",
                     0, MB_OK | MB_CUACRITICAL);
                 return 0;
             }
 
             return 0;

Here is the first user message we get. This message will be sent by the second thread's window procedure to the main one to say it is all setup and ready to go. We respond by telling it to go ahead and start painting the window.

         case WM_ACK:
             WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0);
             return 0;

When the main window gets resized, we stop the second thread from painting, and then restart it, causing it to update itself with the new size of the window. Note that we use WinSendMsg first, then WinPostMsg. We send the message first, which calls the target window procedure directly and will not continue until it returns (thus ensuring the message gets processed) and then we Post the message to restart in the second thread's message queue so it can keep going.

         case WM_SIZE:
             WinSendMsg(hwndWorker, WM_END_PAINT, 0, 0);
 
             WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0);
             return 0;

We get this message when the user drops a font or colour onto the window. We Invalidate the entire window and force a repaint so we can display with the new font/colours.

         case WM_PRESPARAMCHANGED:
             WinInvalidateRect(hwndMain,NULL,TRUE);
             return 0;

If the main window needs to be painted, we simply make sure that the second thread gets on with it. Note again the different use of sending and posting the message.

         case WM_PAINT:
             WinSendMsg(hwndWorker, WM_END_PAINT, 0, 0);
 
             WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0);
             return 0;

In order to simplify things, PM does some of the painting for us if we like - returning true from this message makes PM erase the entire window with the default colour.

         case WM_ERASEBACKGROUND:
             return (MRESULT) TRUE;

Now, whenever a user selects something from the menu, we get a WM_COMMAND message. We can then use the SHORT1FROMMP macro (which means extract a short value from the high word of a message parameter) to get the ID of the menu item selected. The first menu selection we process is the most important one - the About box. We call WinDlgBox which does a lot of work for us. It loads the dialog from the resource (check out STEP02.RC), runs the dialog procedure we specify (which works just like a window procedure) and will not return until the dialog is closed. This is called a modal dialog - it will not allow the user to select anything else until they finish with the dialog. Contrast this with a modeless dialog, which can be used at the same time as any other windows. These are often used for floating toolboxes, etc. We pass WinDlgBox the parent window (which will be the desktop), the owner window (our main window), a pointer to the dialog procedure which handles the messages, a handle to the module where the resource is located (by specifying NULLHANDLE OS/2 looks in the EXE file), and any extra info we want to pass the dialog procedure.

         case WM_COMMAND:
             switch (SHORT1FROMMP(mp1))
             {
                 case ID_ABOUT:
                     WinDlgBox(HWND_DESKTOP,
                               hwnd,
                               (PFNWP)DlgProc,
                               NULLHANDLE,
                               DLG_ABOUT,
                               NULL);
                     return 0;

Now we will use one of OS/2's standard dialogs - the File Open dialog. All we do is set up a structure with the appropriate options, and call WinFileDlg. Once it returns we can examine the FILEDLG struct for the file the user selected. This example only displays the dialog - it does nothing with what the user selected.

                 case ID_FILEOPEN:
                     memset(&fild, 0, sizeof(FILEDLG));
                     fild.cbSize=sizeof(FILEDLG);
                     fild.fl=FDS_OPEN_DIALOG | FDS_CENTER ;
                     fild.pszIDrive="C:";
                     WinFileDlg(HWND_DESKTOP,hwnd,&fild);
                     return 0;

If the user selects Exit from the File menu, we just send ourselves a WM_CLOSE message, which by default will shut down our application.

                 case ID_FILEEXIT:
                     WinPostMsg(hwnd, WM_CLOSE, 0, 0);
                     return 0;

Any other messages we don't need to worry about, so let PM handle them by passing them on to the default PM window procedure.

                 default:
                     return WinDefWindowProc(hwnd,
                                             msg,
                                             mp1,
                                             mp2);
             }

Notice how we first destroy the second thread so it can clean up, before we close ourselves.

         case WM_DESTROY:
             WinSendMsg(hwndWorker, WM_DESTROY,
                        mp1, mp2);
             return 0;
         }
     return WinDefWindowProc (hwnd, msg, mp1, mp2);
 }

Now we get to the interesting bit - our Worker thread. It looks like a normal function, it just gets called differently. Although this example does not cover it, you must keep in mind that having multiple threads in the one program requires some forethought. You can't have two threads trying to write to the one data file, for example. We will explore this problem and how to solve it (using Semaphores) in a later article.

 VOID WorkerThread ()
 {
     HAB  hab;
     HMQ  hmq;
     HWND hwndObject,
          hwndParent;
     QMSG qmsg;

This should look familiar - it looks very much like the main procedure. The only difference is that we specify HWND_OBJECT when we create the window. We need our own message queue so we can talk to the main window. Notice how we ACKnowledge the main window once we get created, and go into our message loop. All the work is actually done in the second thread's window procedure. One special thing to note is the call to _endthread at the end. This lets the C runtime library clean up after us, and shuts down the thread properly.

     hab = WinInitialize (0);
 
     hmq = WinCreateMsgQueue(hab, 0);
 
     WinRegisterClass(hab, "STEP2_B", WorkWndProc, 0, 0);
 
     hwndWorker = WinCreateWindow (HWND_OBJECT,
                                   "STEP2_B", "",
                                   0, 0, 0, 0, 0,
                                   HWND_OBJECT,
                                   HWND_BOTTOM,
                                   0, NULL, NULL );
 
     WinSendMsg( hwndMain, WM_ACK, 0, 0 );
 
     while( WinGetMsg ( hab, &qmsg, 0, 0, 0 ))
         WinDispatchMsg ( hab, &qmsg );
 
     WinPostMsg( hwndMain, WM_QUIT, 0, 0 );
 
     WinDestroyWindow( hwndWorker );
     WinDestroyMsgQueue( hmq );
     WinTerminate (hab);
     _endthread ();
 }

This is the dialog procedure for the About box. It is just the same as a window procedure except that for the messages we don't process, we call WinDefDlgProc instead of WinDefWindowProc because certain things have to be handled differently. If the user presses a button in the dialog, we get a WM_COMMAND much the same as if they selected a menu item. We know that the OK button is the only button that will do anything so we don't bother checking and just close the dialog by calling WinDismissDlg. We pass it the handle of the dialog, and a BOOLean value which will be returned to the calling function (back at WinDlgBox) so we can tell if the user pressed OK or Cancel.

 MRESULT EXPENTRY DlgProc(HWND hwnd, ULONG msg,
                          MPARAM mp1, MPARAM mp2)
 {
     switch (msg)
     {
         case WM_COMMAND:
             WinDismissDlg(hwnd,TRUE);
             return 0;
         default:
             return WinDefDlgProc(hwnd,msg,mp1,mp2);
     }
 }

This is the window procedure for the "dummy" OBJECT window which the second thread creates. We have to set up a few things when we get CREATEd. Firstly, we ask PM for a timer. We give it a handle to the anchor block (which we get from our handle), the handle itself, an ID for the timer (you can have more than one), and the delay in milliseconds between timer "ticks". We ask it to send us a WM_TIMER once a second, so we can update our clock. The next thing we do is get ourselves a Presentation Space (PS), which is like a handle which is used when we want to draw or paint in the window. We then say that we want the background to be cleared when we draw things. Otherwise the time we display would overwrite itself and soon become garbled. I will not go into to much detail on the drawing side of things, as the GPI (Graphics Programming Interface) itself could fill a book (and has).

 MRESULT EXPENTRY WorkWndProc(HWND hwnd,
                              ULONG msg,
                              MPARAM mp1,
                              MPARAM mp2) {
     static BOOL Paint=FALSE;
     static HPS  hps;
     SIZEL       sizel;
 
     switch (msg)
     {
         case WM_CREATE:
             if (!WinStartTimer(WinQueryAnchorBlock(hwnd),
                                hwnd,
                                1,
                                1000))
                 WinMessageBox(HWND_DESKTOP,
                               hwnd,
                               "Could not start timer!",
                               "Error",
                               0,
                               MB_CUACRITICAL | MB_OK);
             hps = WinGetPS(hwndMain);
             GpiSetBackMix(hps, BM_OVERPAINT);
             return 0;

WM_BEGIN_PAINT is our first user message sent to us from the main window. All we do is set a flag saying it is OK to keep painting. If we get WM_END_PAINT then we stop painting for the moment (even while we are still getting WM_TIMER messages).

         case WM_BEGIN_PAINT:
             Paint = TRUE;
             return 0;
 
         case WM_END_PAINT:
             Paint = FALSE;
             return 0;

Every second, we will get a WM_TIMER message, and all we do is check if it is Ok to paint, and then do so.

         case WM_TIMER:
             if (Paint)
                 WorkPaint(hwndMain, hps);
             return 0;

If we get closed, make sure we clean up by stopping our timer and releasing the PS we got to draw with. Cleanup is always very important, and could potentially cause some nasty bugs which may not be obvious. Always consult the PM Reference manual for functions you may use which require resources to be released or destroyed.

         case WM_DESTROY:
             WinStopTimer(WinQueryAnchorBlock(hwnd),
                          hwnd, 0);
             WinReleasePS(hps);
             return 0;
     }
     return WinDefWindowProc (hwnd, msg, mp1, mp2);
 }

This function does the painting for us. We get the size of the main window (a RECTangLe), then calculate its width and height.

 VOID WorkPaint (HWND hwnd, HPS hps)
 {
     ULONG   x, y, cx, cy;
     RECTL   rect;
     POINTL  ptl;
     char    s[42];
     struct time  t;
 
     WinQueryWindowRect(hwnd, &rect);
 
     cx = rect.xRight - rect.xLeft;
     cy = rect.yTop - rect.yBottom;

We check what the current time is, and compose our string.

     gettime(&t);
 
     sprintf(s,"Current Time: %2.2d:%2.2d%colon.%2.2d",
             t.ti_hour,t.ti_min,t.ti_sec);

We want the time to be shown roughly in the middle of the screen, so we figure out where that is. We then randomly pick a colour and display the string at the specified point.

     ptl.x=(cx/3); ptl.y=(cy/2);
     GpiSetColor(hps, rand() % 16);
     GpiCharStringAt(hps, &ptl, strlen(s), s);
 }

Prologue

Well, that's it. It's not a particularly exciting program, and it could probably be more efficient, but it does provide a general skeleton for a multi-threaded PM application. Study the structure of the program, in blocks. The main function which sets up and creates the window, the main window procedure which handles all the messages and delegates work through user messages to the worker thread. Then the worker thread which sets up the object window, and its corresponding window procedure which does all the work.

Try a few things. First, drop a different font on the time. Then try a colour (this will only last until it updates itself, since it changes colour itself). Then bring up the About box and move it so you can see the time still updating itself.

Something we have not discussed is thread states. Basically, a thread can either be running (the currently executing thread of which there can only ever be one), ready to run, or blocked (waiting for something to happen). This makes for very efficient programming. Imagine a terminal program. One thread can be handling the user interface, one doing general work, and another monitoring the serial port for input. It would call DosRead from COM1 for example. If there is nothing to read, it doesn't have to sit in a loop and poll the port. OS/2 will block the thread and drop its priority until DosRead returns with something, so it won't get any CPU time until it needs it.

The possibilities for threads are endless - background recalculating, background printing, etc. are just some. See how threads could improve your programs.

What Next?

There is a lot of ground to cover in this area, and we have only scratched the surface. But fear not - help is at hand! (You'll just have to wait until next month...)

I welcome any feedback on this article (netmail preferred) - any comments or suggestions you may have, questions on this article, or things you would like to see in a future article. I hope you have learned something!

Bibliography

The following references were used in the preparation of this article: