Threads for Windows 3

David Lee

David is a developer for Inference Corp. and can be reached at 550 N. Continental Blvd., El Segundo, CA 90245-5052.


Threads are becoming standard fare in today's PC operating systems. Both Windows NT and OS/2 2.1, for instance, support threading as an intrinsic component of the operating system. Threads make creating interactive programs much easier, while users reap the benefits of faster, more responsive applications.

Windows 3, however, doesn't have built-in support for threads. Nevertheless, as the code presented here shows, you can still implement non-preemptive threads in Windows.

End of the Hourglass

Threads are perfect for handling interactive applications that perform time-intensive computations like printing and updating the screen. An application starts a thread to begin work on the problem, and remains responsive to user input. If the application instead changes the cursor to an hourglass and begins its work without using threads, the user can't use other features of the application. Under Windows 3, other applications are unavailable to the user as well.

There are two conventional methods for keeping compute-intensive applications responsive when threads aren't available. In the first, the application sends timer messages to itself, computing a subset of the operation at each timer event. This method is painstaking for the programmer because the computation is broken in an unnatural way. At each timer event, the program reestablishes its context in the computation, calculates a little more, then saves its state in preparation for the next timer event.

A second common approach is for the application to make occasional calls to the Windows API PeekMessage() during the computation. By calling PeekMessage(), other applications can process events, thus giving the user a chance to do something while waiting. One problem with this technique is that an application can perform only one time-consuming operation at a time. The user can't start a second long operation in the program without putting the first on hold until the second operation completes.

A more serious problem is that occasional calls to PeekMessage() don't give Windows enough of what Microsoft calls "idle time." Windows uses idletime to optimize paging, control power management on portable computers, and process real-mode interrupts for TSRs loaded before Windows. Applications generally give idle time to Windows by calling GetMessage() which, unlike PeekMessage(), doesn't return until an event occurs in the application. Windows uses the time when all applications are waiting in GetMessage() for idle-time processing. When a program instead relies on PeekMessage() to keep Windows responsive, Windows doesn't have sufficient idle time.

My approach to threads for Windows uses a Windows timer to schedule threads, which allows applications to use a standard GetMessage() loop. This gives Windows the idle time it needs.

Handling one or more time-consuming tasks with threads is much easier than with either of the aforementioned conventional approaches. When a user selects an appropriate application feature, the app creates a thread to perform the computation. The application is then immediately ready to handle user input while the thread runs in the background.

Adapting a program to use threads

is generally straightforward. Programs create threads with a function call, passing the address of a thread routine. You must isolate features in the application that should run in the background, then give each feature a single entry point. Threads must occasionally call ThrYield() to give up control of the CPU, since the threading is not preemptive, and they run until either the thread routine returns or the thread terminates.

Details

The thread API includes functions for thread creation, scheduling, and termination. The thread-management code is in the file THR.C. Listing One (page 86) is the include file and Listing Two (page 86) is the actual source for THR.C. Additional files (.OBJ, .RC, and the like) are available electronically; see "Availability," page 3.

Applications create threads using the ThrCreateThread() function. This function takes the address of a thread routine, a DWORD parameter to be passed to the thread routine, the stack size to give to the thread, and a Boolean variable that specifies whether the thread should initially be enabled for running. ThrCreateThread() returns a DWORD thread ID that the application uses to enable and disable the thread as the need arises. At thread-creation time, ThrCreateThread() allocates a stack and places the thread in the pool of threads. The thread will not start to run until the application returns control to Windows.

The thread-management code uses

a Windows timer to handle thread scheduling. If any threads are active in the application, the thread library starts a timer. Each time Windows invokes the timer procedure, the thread library picks a thread from the pool of threads, saves the current machine state, restores the thread's state, and invokes the thread. When the thread yields control, the thread library saves its state, restores the state of the machine to the point before the thread was invoked, then returns control to Windows.

Once a thread finishes, it falls out of its thread routine or calls ThrExit(). At this time, the thread library removes the thread from the pool of threads and frees its stack. It's important that each thread free any system resources it allocated. Otherwise, the resources remain in use until the application exits.

Since the threading is not preemptive, threads must voluntarily give up control of the CPU at regular intervals by calling ThrYield(). This is not particularly painful, as the thread does not have to manually save state information. When the thread receives another time slice, ThrYield() returns and the thread picks up where it left off.

Windows timers are only called a maximum of 18 times a second, so it is important that thread routines accomplish a reasonable amount of work before yielding. Try to shoot for somewhere between 10 and 50 milliseconds between calls to ThrYield(). A little experimentation is necessary to obtain good performance, but you can expect reasonable behavior without much tuning.

Giving up control of the CPU in thread routines has some advantages over preemptive threading. For example, if two threads modify a global data structure, a semaphore would have to be used in a preemptive environment to ensure that the threads don't clobber each other. In this implementation, the integrity of the global data structure is ensured if the threads don't yield during the middle of an update to the data.

Demo Program

The demonstration program, THREAD.C (also available electronically), creates threads that draw lines and circles at random in the application's window. Each time New Line Thread or New Circle Thread is selected from the menu, ThrCreateThread() is invoked and passed the address of a thread routine that draws either lines or circles. A structure passed to the thread routines specifies how many objects to draw and the window handle of the application window.

The thread routines call ThrYield() every time ten objects are drawn. This gives control back to Windows so the system remains responsive. When the thread routine runs again, ThrYield() returns as if nothing happened.

If the user closes the application or selects Stop Threads from the menu, the application sets a static variable to True. The thread routines check the state of this variable before drawing each object, and abort when they see that it is True. The app waits until no more threads exist before returning.

The program was compiled using Visual C++ 1.0, but any Windows C compiler should work.

Conclusion

Threads provide a natural way to perform CPU-intensive tasks within a Windows program while maintaining system responsiveness. The solution presented here provides a method for applications to take advantages of threads in Windows 3 applications.


[LISTING ONE]


typedef void (FAR * THREADPROC)(DWORD dwParam);
int ThrCreateThread(THREADPROC tp,DWORD dwParam,UINT uiStackSize,
                    BOOL bEnable,DWORD FAR *pdwID);
int ThrYield(void);
int ThrExit(void);
int ThrThreadCount(UINT FAR *puiCount);
int ThrAppYield(void);
int ThrEnableThread(DWORD dwID,BOOL bEnable);

[LISTING TWO]


// Threads for Windows. -- Do what you like with these sources.
// Version 0.2 -- // David Lee -- dlee@inference.com
// Compiled with Visual C++ 1.00.

#define NOKEYSTATES
#define NODRAWTEXT
#define NOLOGERROR
#define NOOEMRESOURCE
#define NOOPENFILE
#define NOTEXTMETRIC
#define NODRIVERS
#define NODBCS
#define NOSYSTEMPARAMSINFO
#define NOSCALABLEFONT
#define NOGDICAPMASKS
#define NOATOM
#define NOMETAFILE
#define NOSCROLL
#define NOSOUND
#define NOCOMM
#define NOKANJI
#define NOPROFILER
#define NODEFERWINDOWPOS

#define STRICT
#include <windows.h>

#include "thr.h"
typedef struct _THREADINFO
{
  WORD ss;                         // stack selector of thread
  WORD sp;                         // stack pointer of thread
  BOOL bCalledYet;                 // TRUE if thread has been started
  BOOL bEnabled;                   // TRUE if thread can run
  DWORD dwParam;                   // passed to thread routine tpStart
  THREADPROC tpStart;              // address of thread routine
  GLOBALHANDLE ghStack;            // stack global handle
  GLOBALHANDLE gh;                 // this structure's global handle
  struct _THREADINFO FAR *ptiNext; // next thread in the list
} THREADINFO, FAR *PTHREADINFO;

static PTHREADINFO s_ptiList=NULL;    // linked list of threads
static PTHREADINFO s_ptiCurrent=NULL; // currently running thread
static BOOL s_bThreadRunning=FALSE;   // TRUE if a thread is running
static UINT s_uiTimerID=0;            // timer identifier
static UINT s_uiThreadCount=0;        // # of threads
static UINT s_uiRealSS;               // task SS register buffer
static UINT s_uiRealSP;               // task SP register buffer
static UINT s_uiTmpSS;                // thread SS register buffer
static UINT s_uiTmpSP;                // thread SP register buffer
static THREADPROC s_tpTmp;            // buffer for thread start address
static DWORD s_dwTmp;                 // buffer for thread parameter

#define SAVE_CPU_STATE __asm \
  {                          \
    __asm pusha              \
    __asm push es            \
  }
#define RESTORE_CPU_STATE __asm \
  {                             \
    __asm pop es                \
    __asm popa                  \
  }
// Visual C++ saves and restores di and si in opposite
// order depending on whether debug is turned on.
#ifdef _DEBUG
#define POP_DI_AND_SI __asm \
  {                         \
    __asm pop di            \
    __asm pop si            \
  }
#else // _DEBUG
#define POP_DI_AND_SI __asm \
  {                         \
    __asm pop si            \
    __asm pop di            \
  }
#endif // _DEBUG
void __loadds __export CALLBACK ThreadTimerProc(HWND hwnd,UINT msg,UINT uiID,
                                                DWORD dwTime);
static void TurnTimerOnOrOff(void);
static void NukeThread(PTHREADINFO pti);
// If any threads exist and are enabled, make sure the timer
// is running.  Otherwise, kill the timer if it is running.
static void TurnTimerOnOrOff(void)
{
  PTHREADINFO pti;
  BOOL bAny;
  for (bAny=FALSE,pti=s_ptiList; !bAny && pti; pti=pti->ptiNext)
    if (pti->bEnabled)
      bAny = TRUE;
  if (bAny && !s_uiTimerID)
    {
      // At least one enabled thread, so start the timer
      s_uiTimerID = SetTimer(0,0,0,(TIMERPROC) ThreadTimerProc);
    }
  else if (!bAny && s_uiTimerID)
    {
      // No enabled threads, so kill the timer
      KillTimer(0,s_uiTimerID);
      s_uiTimerID = 0;
    }
} /*TurnTimerOnOrOff*/
// Free storage associated with a thread that is being removed.
static void NukeThread(PTHREADINFO pti)
{
  GLOBALHANDLE gh;
  PTHREADINFO ptiTmp;
  // Get rid of the thread's stack
  GlobalUnlock(pti->ghStack);
  GlobalFree(pti->ghStack);
  // Remove thread structure from the linked list of threads
  if (s_ptiList == pti)
    s_ptiList = pti->ptiNext;
  else
    {
      for (ptiTmp=s_ptiList; ptiTmp; ptiTmp=ptiTmp->ptiNext)
        if (ptiTmp->ptiNext == pti)
          {
            ptiTmp->ptiNext = pti->ptiNext;
            break;
          }
    }
  gh = pti->gh;
  GlobalUnlock(gh);
  GlobalFree(gh);

  TurnTimerOnOrOff();
  s_uiThreadCount--;
  s_ptiCurrent = NULL;
  s_bThreadRunning = FALSE;
} /*NukeThread*/
// This callback is called by Windows for each timer event.
// It picks a thread to give a shot at the CPU and runs it.
void __loadds __export CALLBACK ThreadTimerProc(HWND hwnd,UINT msg,UINT uiID,
                                                DWORD dwTime)
{
  UINT uiPass;
  PTHREADINFO pti,ptiTmp;
  // The timer callback can be called when a thread is running -- don't recurse
  // because this code _and_ the thread code don't expect it.
  if (!s_bThreadRunning && s_uiThreadCount)
    {
      s_bThreadRunning = TRUE;
      // Pick a thread to run, usually the thread after the last thread run.
      ptiTmp = s_ptiCurrent ? s_ptiCurrent : s_ptiList;
      for (pti=NULL,uiPass=0; !pti && uiPass<=s_uiThreadCount; uiPass++)
        {
          ptiTmp = ptiTmp->ptiNext ? ptiTmp->ptiNext : s_ptiList;
          if (ptiTmp->bEnabled)
            pti = ptiTmp;
        }
      if (pti)
        {
          s_ptiCurrent = pti;
          if (pti->bCalledYet)
            {
              // Save the real task's state
              SAVE_CPU_STATE;
              __asm mov s_uiRealSS, ss
              __asm mov s_uiRealSP, sp
              // Restore the thread's state.
              // Can't dereference pti in __asm statement.
              s_uiTmpSS = pti->ss;
              s_uiTmpSP = pti->sp;
              __asm mov ss, s_uiTmpSS
              __asm mov sp, s_uiTmpSP
              RESTORE_CPU_STATE;
              // Return back to thread as if we are returning from ThrYield().
              POP_DI_AND_SI;
              __asm leave
              __asm retf
            }
          else
            {
              pti->bCalledYet = TRUE;
              s_tpTmp = pti->tpStart;
              s_dwTmp = pti->dwParam;
              // Save the real task's state
              SAVE_CPU_STATE;
              __asm mov s_uiRealSS, ss
              __asm mov s_uiRealSP, sp
              // Set up the thread's stack
              s_uiTmpSS = pti->ss;
              s_uiTmpSP = pti->sp;
              __asm mov ss, s_uiTmpSS
              __asm mov sp, s_uiTmpSP
              // Kick off the thread
              s_tpTmp(s_dwTmp);
              // Thread routine returned.  It is finished, so restore
              // the task's real state and delete the thread.
              __asm mov ss, s_uiRealSS
              __asm mov sp, s_uiRealSP
              RESTORE_CPU_STATE;
              NukeThread(pti);
            }
        }
      else s_bThreadRunning = FALSE;
    }
} /*ThreadTimerProc*/
// The thread requests termination, so get rid of it and continue on. This
// should only be called by a thread, not by the main app thread.
int ThrExit(void)
{
  if (s_bThreadRunning) // make sure
    {
      // Restore state of machine to the point before thread was invoked.
      __asm mov ss, s_uiRealSS
      __asm mov sp, s_uiRealSP
      RESTORE_CPU_STATE;

 NukeThread(s_ptiCurrent);
      // Return to Windows as if we are returning from ThreadTimerProc().
      POP_DI_AND_SI;
      __asm leave
      __asm retf 0xa
    }
  return(0);
} /*ThrExit*/
// This should be called by threads every .05 to .1 seconds.
// This routine yields the CPU to other threads and other apps.
int ThrYield(void)
{
  if (s_bThreadRunning) // make sure
    {
      // Save the thread's state.  Can't dereference s_ptiCurrent
      // in __asm statement, so store in a temporary buffer.
      SAVE_CPU_STATE;
      __asm mov s_uiTmpSS, ss
      __asm mov s_uiTmpSP, sp
      s_ptiCurrent->ss = s_uiTmpSS;
      s_ptiCurrent->sp = s_uiTmpSP;
      // Restore state of machine to point before the thread was invoked.
      __asm mov ss, s_uiRealSS
      __asm mov sp, s_uiRealSP
      RESTORE_CPU_STATE;
      s_bThreadRunning = FALSE;
      // Return to Windows as if we are returning from ThreadTimerProc().
      POP_DI_AND_SI;
      __asm leave
      __asm retf 0xa
    }
  return(0);
} /*ThrYield*/
// Allows an application to yield to its threads without dropping into app's
// main event queue.  Returns TRUE if a thread was run.
int ThrAppYield(void)
{
  MSG msg;
  int iRet=FALSE;
  if (PeekMessage(&msg,NULL,WM_TIMER,WM_TIMER,PM_NOREMOVE))
    if (GetMessage(&msg,NULL,WM_TIMER,WM_TIMER))
      {
        iRet = TRUE;
        DispatchMessage(&msg);
      }
  return(iRet);
} /*ThrAppYield*/
// Turns a thread on/off without killing the thread.
int ThrEnableThread(DWORD dwID,BOOL bEnable)
{
  PTHREADINFO pti = (PTHREADINFO) dwID;
  pti->bEnabled = bEnable;
  TurnTimerOnOrOff();
  return(0);
} /*ThrEnableThread*/
// Returns the number of threads that currently exist.
int ThrThreadCount(UINT FAR *puiCount)
{
  *puiCount = s_uiThreadCount;
  return(0);
} /*ThrThreadCount*/
// Kick off a thread.  Return 0 if OK, nonzero otherwise.
//            tp: Address of the thread procedure (cdecl)
//       dwParam: Passed to the thread procedure
//   uiStackSize: Amount to allocate for the stack (or 0 for 8192)
//       bEnable: TRUE if thread should be initially runnable
//         pdwID: returns the thread ID
int ThrCreateThread(THREADPROC tp,DWORD dwParam,UINT uiStackSize,
                    BOOL bEnable,DWORD FAR *pdwID)
{
  GLOBALHANDLE gh;
  PTHREADINFO pti;
  int iRet=0;
  *pdwID = 0L;
  // Allocate empty thread structure and get it ready to run.
  if (gh = GlobalAlloc(GHND,sizeof(THREADINFO)))
    {
      pti = (PTHREADINFO) GlobalLock(gh);
      pti->gh = gh;
      if (!uiStackSize)
        uiStackSize = 8192;
      if (gh = GlobalAlloc(GHND,uiStackSize))
        {
          *pdwID = (DWORD) pti;
          s_uiThreadCount++;
          pti->ghStack = gh;
          pti->tpStart = tp;
          pti->ss = HIWORD(GlobalLock(gh));
          pti->sp = uiStackSize;
          pti->bCalledYet = FALSE;
          pti->dwParam = dwParam;
          pti->bEnabled = bEnable;
          // Put the new thread at the head of the linked list of threads.
          pti->ptiNext = s_ptiList;
          s_ptiList = pti;
          TurnTimerOnOrOff();
        }
      else
        {
          iRet = 2;
          gh = pti->gh;
          GlobalUnlock(gh);
          GlobalFree(gh);
        }
    }
  else iRet = 1;
  return(iRet);
} /*ThrCreateThread*/                                         End Listings




Copyright © 1993, Dr. Dobb's Journal