6. Concurrency and Synchronization

Since the appearance of multi-processor computers and multi-tasking operating systems, programmers have been interested in concurrent programming-- doing several tasks in parallel, then putting the pieces together at the end to produce a final answer. Even on single-processor computers concurrent programs offer advantages, because much of the scheduling and control information that would be needed in a traditional program is managed by the underlying operating system, instead; so programmers only need to concentrate on describing the independent sub-tasks that must be performed.

Multi-Threading

There are problems with concurrency, however. The biggest problem is that if each task is to be performed by a separate program, all running on the same single-processor computer, then we will have to add the time it takes the operating system to switch the processor from one program to another to the overall processing time. This time can be considerable, because when control of the CPU is switched from program A to program B, then some or all of B's data and control information may need to be restored from spacious, low-speed memories to cramped, high-speed memories, while some or all of A's data and control information may need to be saved from the high-speed memories back to the low-speed memories.

Many operating systems solve this problem for us. Instead of implementing a concurrent algorithm as a loose collection of collaborating programs, programmers can implement it as a single master program, which "fathers" several "child programs" that perform the various sub-tasks in parallel, then report their results back to the master. Child programs are called threads, or light-weight processes.

What makes this idea an improvement is that a thread isn't a full fledged program with its own data and control information. Instead, the thread simply uses the data and control information of its parent program (or most of it, anyway). This means switching the CPU from one thread to another is relatively efficient.

Implementing and Scheduling Threads

An object-oriented operating system might represent each thread as an object that "controls" other objects representing system resources such as memory, processors, or I/O devices. In fact processes, the objects that represent ordinary programs, are simply threads that control additional resources:

In a simplified operating system, a thread is always in one of at least four states: Ready (waiting for the CPU), Running (using the CPU), Blocked (waiting for input), or Terminated. We can represent these states, the transitions between them, and the actions that cause transition to occur using a state diagram:

It is the job of an operating system component called the scheduler to perform the switch and preempt actions. Switching means switching control of the processor from the thread that currently controls the processor to the next thread in the ready queue. All threads in the ready queue are in the Ready state, because they are ready to use the processor. A thread that controls a processor is in the Running state, because it uses its processor to run.

What makes a running thread give up control of a processor? When the task performed by a thread is complete, then the thread stops. Also, the operating system will require a running thread to release its processor and enter a Blocked state while it waits for input to arrive. Some operating systems will even preempt a thread that attempts to starve its relatives by consuming large slices of processor time without releasing it to the others. Finally, cooperative threads voluntarily release processor control on occasion, either by auto-transitioning back to their Ready state, or by auto-transitioning to their Blocked state for a specific amount of time (sleeping).

Inter-Thread Communication

Naturally, the more interesting concurrent programs are the ones where the threads collaborate in interesting ways. Obviously, meaningful collaboration requires communication. But how can threads communicate? Broadly speaking, there are two techniques: message passing and shared memory.

In some cases a parent thread might provide message brokering services for its children. Essentially, the parent thread acts like a post office: a child thread gives the parent a message, which the parent eventually forwards to the recipient child thread. Of course this is very similar to the job of the message pump of a Windows application.

While using the parent as a message broker promotes decoupling among the child threads, it introduces two inefficiencies. First, all messages must pass through a middle man-the broker. Second, there will have to be some limit on the amount of information that can be sent in a single message.

All child threads can easily access their parent thread's global data structures. This means they can communicate directly through these data structures: if thread A changes the engine design on a global CAD/CAM model of a car, then thread B will be able to see this change, and therefore will take it into account as it estimates the total cost of the car.

In effect, global data structures act like abstract mailboxes. Thread A places a "message" for thread B in mailbox M. Later, thread B removes the message and reads it. No middle man is required, and messages can hold as much information as we like. But the downside is this: what happens if thread B checks mailbox M before thread A posts the message? In this case B may not get the message, which may result in an error.

Sometimes it's difficult for programmers to cope with problems like this, because they are accustomed to being able to predict the execution order of a sequence of instructions. While the execution order of instructions being executed by a single thread is predictable, the execution order of instructions being executed by different threads isn't.

Once again the operating system comes to our rescue, this time by supplying synchronization mechanisms: objects that work like latches and locks. If we equip mailbox M with a locked latch, L, then if Thread B doesn't have a "key", it must wait next to the mailbox for the latch to be unlocked. Of course this will only happen after thread A, which does have a key, puts its message inside M.

Active Objects

Combining multi-threading and object-orientation produces the powerful idea of an active object: an object that "owns" a thread of control. Until now, all of the objects we have dealt with have been passive. A passive object does nothing unless a client calls one of its member functions. When the member function terminates, the passive object goes back to doing nothing. But an active object doesn't need to wait for a client to call one of its member functions. Instead, it can use its thread to execute a control loop that perpetually searches for work to be done:

1. Inspect environment
2. Compare environment state to goal state
3. If same, quit
4. Update environment
5. Repeat

In effect, an object-oriented concurrent program can be seen as a society of active objects, each behaving like a tiny virtual machine that drives the application toward a goal. Of course there may be many active objects, each with its own goal. In some cases these goals may even conflict with each other, but the overall effect is that the application is collectively driven toward some larger goal.

Active objects are particularly useful in simulations of systems containing autonomous, active elements.

The Master-Slave Design Pattern

Designing a program as a society of active objects can lead to chaos. To avoid this fate, most multithreaded applications instantiate some variant of the Master-Slave design pattern:

Master-Slave [POSA]

Problem

An identical computation must be performed many times, but with different inputs and context. The results of these computations may need to be accumulated. If possible, we would like to take advantage of multiple processors.

Solution

A master thread creates multiple slave threads. Each slave performs a variation of the computation, reports the result to the master, then terminates. The master accumulates the results.

Assume threads are represented as instances of a Thread class. Then an active object is an instance of a class derived from or associated with the Thread class:

In the Master-Slave pattern active master object creates many active slave objects. Each slave object retains a pointer back to its master:

Each slave performs some task, then reports its result back to the master. The master accumulates results and produces a final result.

Multi-Threading in Windows

Like a window or a file, a thread is a system resource that must be created, managed, and destroyed by the operating system. We have seen that like most operating systems, Windows uses the Wrapper-Body design pattern to protect these resources. For example, an application-level C++ object representing a window is merely a wrapper that encapsulates a handle or identification number of a body, which in this case is a system-level C "object" that is the actual window data structure. Most wrapper member functions simply delegate to the corresponding body member functions through the Window Manager, which filters out unauthorized requests. Recall that CWnd is the MFC class of all window wrappers.

The story for threads is the same. Instances of MFC's CWinThread class are merely application-level wrappers that forward most client requests through the operating system's thread manager to system-level objects that represent the actual thread data structures. Of course the thread manager filters out requests that it deems to be illegal, unauthorized, or dangerous. We will use the terms "application thread" and "system thread" to distinguish between CWinThread wrappers and their corresponding system-level bodies.

Here is an edited version of MFC's CWinThread class declaration that shows some of the important public members:

class CWinThread : public CCmdTarget
{
public:
   CWinThread();             // creates app thread
   BOOL CreateThread(...); // creates system thread
   CWnd* m_pMainWnd;       // the app's main window
   HANDLE m_hThread;         // this thread's HANDLE
   MSG m_msgCur;            // the current message
   LPVOID m_pThreadParams;             // generic parameters
   AFX_THREADPROC m_pfnThreadProc;   // = 0 if UI thread

   int GetThreadPriority();
   BOOL SetThreadPriority(int nPriority);

   DWORD SuspendThread();
   DWORD ResumeThread();

   virtual int Run();
   virtual BOOL PumpMessage();
   virtual BOOL OnIdle(LONG lCount);
   BOOL PostThreadMessage(...);
   // etc.
};

Programmers must be careful to distinguish between the lifetime of an application thread and the lifetime of the corresponding system thread. Like all C++ objects, an application thread is created by an explicit or implicit call to the CWinThread() constructor. By contrast, a system thread is created from an application thread by a call to the CreateThread() member function, which calls the global CreateThread() function of the Win32 API. If a system thread is successfully created, a handle for this thread is stored in the application thread's m_hThread member variable.

For example, assume Master is a programmer-defined class derived from CWinThread:

class Master: public CWinThread { ... };

Creating and starting an instance of Master is a two-step process:

Master* pThread = new Master(); // create application thread
pThread->CreateThread(); // create & start system thread

Normally, a system-thread joins the ready queue as soon as it is created. Its place in the queue is determined by its priority, which can be any of the following pre-defined integers:

THREAD_PRIORITY_TIME_CRITICAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_IDLE

When the thread gains control of the CPU, it starts or resumes its controller function, which is either the default control loop, Run(), or some custom controller pointed at by the laboriously named m_pfnThreadProc member variable.

In addition to requesting input or being preempted by the operating system, there are several other ways that a thread looses control of the CPU:

By calling the SuspendThread() member function. In this case another thread must call the suspended thread's ResumeThread() member function, which moves the suspended thread back into the ready queue.

When the controller function terminates. Normally, this causes the operating system to destroy the system thread, although the corresponding application thread exists until the programmer explicitly or implicitly destroys it.

By calling the global Sleep() function. After the specified time interval has elapsed, the thread moves back into the ready queue.

User Interface Threads

Unless the programmer specifies a different controller function, a running thread executes the pre-defined Run() member function. This function perpetually monitors a message queue associated with the thread. The operating system and application objects can post messages to this queue by calling the PostThreadMessage() member function. When a message arrives, Run() forwards the message to its target.

More specifically, Run() perpetually oscillates between idling and message pumping. As long as there are no messages in the message queue, which is indicated when the global PeekMessage() function returns false, Run() repeatedly calls the OnIdle() member function. The default implementation of this function does nothing at all, but it is a virtual function, which can be redefined in derived classes to perform background tasks from spell checking to searching for radio signals from extra terrestrials.

When a message arrives, the control loop shifts into its message pumping phase, where it repeatedly calls the PumpMessage() member function until the WM_QUIT message arrives and the thread terminates, or until the message queue is empty and the control loop can return to its idle phase:

int CWinThread::Run()
{
   BOOL bIdle = TRUE;
   for (;;)
   {
      // phase1: idle until a message arrives
      while (bIdle && !::PeekMessage(&m_msgCur, ...))
         if (!OnIdle(...)) bIdle = FALSE;
      // phase2: pump messages while available
      do
      {
         if (!PumpMessage()) return ExitInstance();
         if (IsIdleMessage(&m_msgCur)) bIdle = TRUE;
      } while (::PeekMessage(&m_msgCur, ...));
   }
}

PumpMessage() extracts a message from the message queue using the global GetMessage() function. The message is stored in the m_msgCur member variable. If the message is WM_QUIT, then PumpMessage() returns FALSE, which terminates Run() and therefore the thread. Otherwise, a pointer to the message is passed to the global TranslateMessage() function, which asks the operating system to elaborate on certain types of messages, then the message is forwarded to its target by the global DispatchMessage() function:

BOOL CWinThread::PumpMessage()
{
   if (!::GetMessage(&m_msgCur, ...)) return FALSE;
   ::TranslateMessage(&m_msgCur); // change WM_KEYDOWN to WM_CHAR
   ::DispatchMessage(&m_msgCur);
   return TRUE;
}

A message is simply a structure with fields indicating its target, type, and additional information such as queuing time, mouse position, and optional content parameters:

typedef struct
{
   HWND hwnd;         // message target handle
   UINT message;      // message type, e.g. WM_PAINT
   WPARAM wParam;      // first content parameter
   LPARAM lParam;      // second content parameter
   DWORD time;         // queueing time
   POINT pt;         // mouse coordinates
} MSG;

Dispatching a message to its target means calling the target object's designated message handler function. Every thread maintains a table called a message map, which contains associations between message types and message handler functions. DispatchMessage() simply searches this table for the appropriate handler and calls it. Programmers can register a handler with a message type using a special member function:

void CWinThread::On_Thread_Message(MESSAGE, HANDLER)

CWinApp

Threads that use the default Run() function are called user interface threads. Although such a thread could be used as a message broker for any collection of objects, the most common use is to dispatch mouse and keyboard messages coming from the user interface.

In fact, the message broker of every MFC application is an instance of a class derived from the CWinApp class, which itself is derived from CWinThread:

class CWinApp : public CWinThread
{
public:
   HINSTANCE m_hInstance; // current running instance
   HINSTANCE m_hPrevInstance; // registry key
   CDocManager* m_pDocManager; // holds doc-view templates
     virtual BOOL InitApplication(); // register app
   virtual BOOL InitInstance(); // redefine in derived class
   virtual int ExitInstance(); // clean up
   // etc.
};

If we use the MFC App Wizard to create an application named "Test", then in addition to CTestView and CTestDoc, the App Wizard also generates CTestApp, a class derived from CWinApp:

class CTestApp : public CWinApp
{
public:
   CTestApp();
   virtual BOOL InitInstance(); // make frame, view, & doc
   afx_msg void OnAppAbout(); // handler for Help/About menu item
   // etc.
};

In this example the App Wizard places the declaration of CTestApp in a file named Test.h. The Test.cpp file contains the declaration of a global instance of the CTestApp class called theApp:

CTestApp theApp;

theApp is an application-level thread. It represents the application's master thread. theApp is a user interface thread. It will perpetually forward messages from the user interface to instances of CTestView, CTestDoc, and other application classes. If CTestView and CTestDoc are the view and model components of the Model-View-Controller architecture, then we can think of theApp as the main controller component. (In general, there are many controllers, but only theApp actively listens for incoming messages. The others passively wait for theApp to call their message handler functions.)

The entry point for a C++ program is a global function called main(), but for a Windows application, the entry point is called AfxWinMain(). MFC programmers rarely see this function because it is buried deep inside the framework. Here it is:

int AfxWinMain(...)
{
   CWinApp* pApp = AfxGetApp(); // = &theApp
   if (!hPrevInstance) // if first run
      pApp->InitApplication(); // register window
   pApp->InitInstance(); // create frame, document, & view
   return pApp->Run(); // message loop
}

The Windows operating system maintains a hierarchical database called the registry, where information about applications, hardware, and users is stored. We can browse the registry using the Registry Editor. (Type regedit into the [Start]/[Run]/[Open] window. Be careful not to change anything. This could damage your system.)

The first time an application runs, a permanent entry is created in the registry. Information about the application's appearance and user preferences will be stored in this entry. Each time the application runs, a temporary entry is created where information about the application's current state is stored.

The first time the MFC version of AfxWinMain() is called, it calls InitApplication(), which creates the permanent registry entry. Next, instances of CTestDoc, CTestView, and CMainFrame are created by the call to InitInstance(). These objects are bound together in a data structure called a document template. Finally, the message loop is started. Test is now listening for mouse and keyboard inputs.

Worker Threads

A thread that is controlled by the default Run() member function is called a user interface thread, so called because this function perpetually listens for and dispatches user interface messages. Of course most applications only have one user interface, and therefore only need one user interface thread. A typical multi-threaded application might instantiate the Master-Slave design pattern. In this case the master thread might be a user interface thread, while slave threads perform various "background" tasks. Microsoft refers to slave threads using the more politically correct term "worker thread".

There are two ways to create worker threads. Programmers can redefine the Run() method in a CWinThread-derived class, or they can create an application thread with an alternate controller function.

Recall that CWinThread has two mysterious member variables:

class CWinThread : public CCmdTarget
{
public:
   LPVOID m_pThreadParams;             // generic parameters
   AFX_THREADPROC m_pfnThreadProc;   // = 0 if UI thread
   // etc.
};

Normally, these pointers are set to 0, the null pointer. Alternatively, the second parameter can point to a global controller function and the first parameter can point to a global data structure. Starting such a thread calls the global controller function instead of Run(). The pointer to the global data structure is passed as a parameter to the global function.

For example, suppose we want to create a worker thread that will perform a specific background task. We begin by describing this task as a global function parameterized by a void pointer and returning an unsigned integer:

UINT SlaveTask(LPVOID pParam) { ... }

A void pointer can point to anything. In this case, it might point to some sort of data structure containing additional information that will be needed by the thread:

SlaveData* pData = new SlaveData(...);

Next, we create an application thread using the CWinThread constructor that initializes the threadProc and threadParams member variables with the addresses of our thread task and data:

CWinThread* pThread = new CWinThread(SlaveTask, pData);

Finally, we create and launch the corresponding system thread:

pThread->CreateThread();

Noticing that the application thread's threadProc member isn't null, the system thread executes ThreadTask() instead of Run().

A Reusable Worker Thread Class

Creating worker threads that execute global functions operating on global data structures isn't a very object-oriented approach. A better idea is to define a reusable WorkerThread class that programmers can customize by overriding a single virtual function. Either the WorkerThread class is derived from the CWinThread class and redefines the inherited Run() function, or the WorkerThread class is a wrapper for the CWinThread class (i.e., a wrapper for a wrapper). The second approach is left as an exercise for the reader.

Our worker thread class is derived from CWinThread, and overrides the inherited Run() function. Initially, it will be used in an application called Bounce, which will be described in detail later. We begin by creating a single document project:

1. Use the MFC App Wizard to create a single document project called Bounce. Accept all other default options.

Using the [New Class] dialog to insert new classes into a project automatically creates header and implementation files filled with the appropriate skeletal declarations:

2. Use the [Insert]/[New Class ...] dialog box to create an MFC class called "WorkerThread" derived from CWinThread.

In addition to being derived from CWinThread, a worker thread also encapsulates a protected pointer to its master, another CWinThread. This pointer is initialized by the constructor. A public Start() function creates and starts the associated system thread. The re-defined Run() function will be called automatically at this point. This function repeatedly calls a protected Update() function that will be redefined in derived classes. The Stop() function terminates the Run() function. A few other member variables will be explained later.

3. Using the file editor, add declarations of member variables called running, delay, and master to the WorkerThread class declaration. Also add member functions called Update(), Run(), Stop(), and Start(), as well as a master pointer parameter to the WorkerThread constructor. Most of these functions can be implemented as inline functions.

Here is a partial listing of the WorkerThread class that shows all of the members we have added:

class WorkerThread : public CWinThread
{
private:
   bool running; // controls run()
protected:
   WorkerThread(CWinThread* m = 0);
   virtual ~WorkerThread() { Stop(); }
   int delay; // sleep time
   CWinThread* master; // my creator
   virtual bool Update() { return false; }
public:
   int Run(); // overrides inherited message loop
   int Stop()
   {
      if (running)
      {
         running = false;
         return ExitInstance();
      }
      return -1;
   }
   void Start()
   {
      if (!running)
      {
         running = true;
         CreateThread();
      }
   }
   // etc.
};

The WorkerThread constructor initializes the master pointer to the parameter, provided one is specified. Otherwise, the master is assumed to be the user interface thread of the application, theApp. The global AfxGetApp() function can be called to fetch the address of theApp. Each user interface thread maintains a pointer to the main window of the user interface, m_pMainWnd. Sometimes worker threads will also need a pointer to this window, so our constructor redefines the inherited m_pMainWnd to point to the main window of its master:

WorkerThread::WorkerThread(CWinThread* m /* = 0 */)
{
   master = m? m: AfxGetApp(); // = m or &theApp
   delay = 50; // millisecs
   running = false;
   // redefine inherited main window pointer:
   if (master) m_pMainWnd = master->GetMainWnd();
}

The Run() function repeatedly calls Update() and sleeps for 50 milliseconds. Sleeping guarantees that other threads won't be starved by a greedy worker thread. There are two ways for the loop to terminate, either the running flag is set to false by calling Stop(), or the Update() function returns false indicating that it doesn't want to be called again:

int WorkerThread::Run()
{
   while(running && Update())
      Sleep(delay); // be cooperative
   return Stop();
}

Of course our implementation of Update() returns false immediately, but this is a virtual function that can be redefined in classes derived from WorkerThread.

Bounce

The Bounce application we have already begun allows users to create "balls" that "bounce" around inside the view window. A ball is created simply by clicking somewhere in the window. The new ball veers off in a random direction and at a random speed.

Pressing the [s] key causes all of the balls to suspend bouncing. Pressing the [r] key causes the suspended balls to resume bouncing. Pressing the [k] key kills all of the balls.

Designing Bounce

The design of Bounce is simple. Each ball is controlled by a separate worker thread and encapsulates the x- and y-coordinates of its position in the view window as well as its speed in the x and y directions. The Update() function updates the position of a single ball, then invalidates the view window:

xc += dx;
yc += dy;
view->Invalidate();

Since there is only a single view, it can maintain a dynamic array of pointers to all of the active balls. Each ball is a slave to the master thread, theApp, which is an instance of the CBounceApp class created by the App Wizard. Following this pointer allows a ball to navigate to the view. This will be useful for deciding when the ball is about to leave the view window and should reverse direction.

Implementing Bounce

Each ball is an instance of a Ball class, which is derived from the WorkerThread class.

3. Use the [Insert]/[New Class ... ] dialog to add a generic class called Ball to the Bounce project. Specify WorkerThread as the base class.

In our implementation the size, color, and maximum speed of all balls are the same. These attributes are controlled by macro definitions in Ball.h.

4. Add constants to the top of Ball.h to control the color, size, and maximum speed of the balls.

Here are the values we will be using:

// ball diameter:
#define DIAM 10
// ball color:
#define RED 255
#define GREEN 0
#define BLUE 255
// maximum speed in x or y direction:
#define MAX_SPEED 6

Although size, color, and maximum speed are uniform, position and speed vary from one ball to another. Therefore, this information should be stored in Ball member variables.

5. Add private integer member variables representing the speed and position of a ball. Add parameters to the Ball constructor that can be used to initialize the position variables. Add getter functions for accessing the position variables. Also add a declaration for the Update() function.

Here is a partial listing of the Ball class:

class Ball : public WorkerThread
{
public:
   Ball(int x = 0, int y = 0);
   virtual ~Ball() {}
   int GetXC() { return xc; }
   int GetYC() { return yc; }
   bool Update();
private:
   int xc, yc; // x & y position
   int dx, dy; // x & y speed
};

Notice that we have assigned default arguments to the Ball constructor parameters. This allows the constructor to be used as the default constructor. Without these default arguments, the Ball class would have no default constructor. Because default constructors can be needed in unexpected places, all classes should have them. The implementation of the Ball constructor uses a random number generator to assign a random speed and direction to newly created balls.

6. The Ball constructor uses its parameters to initialize its xc and yc member variables. dx and dy are initialized to random, non-zero numbers between -MAX_SPEED and +MAX_SPEED. Include <math.h> at the top of Ball.cpp to declare the standard exponentiation function, pow().

Here is a listing for the constructor:

Ball::Ball(int x, int y)
{
   xc = x;
   yc = y;
   // velocity is random:
   int randomX = rand() % MAX_SPEED;
   int randomY = rand() % MAX_SPEED;
   dx = int(pow(-1, randomX) * (randomX + 1));
   dy = int(pow(-1, randomY) * (randomY + 1));
}

One advantage of multi-threading is that it allows us to specify the behavior of a single control loop cycle of a single thread. We don't need to worry about scheduling or iterating, because this is built into the system. For example, in the case of a ball, we only need to specify how a single ball moves from its current position to its next position. This is the content of the Update() function.

7. Implement the Update() function so that it modifies the position, then invalidates the smallest rectangle in the view window that contains the former and present bounding rectangles of the ball. Finally, reverse the sign of dx or dy if the ball is moving out of the view window.

The Update() function begins by computing the bounding rectangle of the ball at its present location:

bool Ball::Update()
{
   // former bounding box:
   CRect oldRect(xc, yc, xc + DIAM, yc + DIAM);
   oldRect.NormalizeRect();

Next, the position of the ball is incremented by the velocity, and the bounding rectangle of the ball in its new position is computed:

   // update position:
   xc += dx;
   yc += dy;
   // new bounding box:
   CRect newRect(xc, yc, xc + DIAM, yc + DIAM);
   newRect.NormalizeRect();

The invalid rectangle is the union of oldRect and newRect. We can obtain a pointer to the view by calling the GetActiveView() member function of the main window (recall that a pointer to the main window was initialized in the WorkerThread constructor). We pass the invalid rectangle to the view's InvalidateRect() function. This will cause the rectangle containing the former and current positions of the ball to be redrawn. The effect will be to erase the ball and redraw it at its new position:

   // invalid rectangle:
   CRect invRect;
   invRect.UnionRect(newRect, oldRect);
   invRect.NormalizeRect();
   // invalidate in active view of main window:
   CView* view = ((CFrameWnd*)m_pMainWnd)->GetActiveView();
   view->InvalidateRect(invRect);

We can compare the top, left, bottom, and right of the view window with the position of the ball. If the ball is moving too far to the left or right, then the sign of dy is changed and a beep will enhance the bouncing effect. Similarly, if the ball is too high or too low, the sign of dx is changed and a beep noise is made:

   // change direction?
   CRect arena;
   view->GetClientRect(&arena);
   arena.NormalizeRect();
   // bottom & right are too big:
   if (xc <= arena.left || arena.right <= xc)
   {
      dx *= -1; // change x direction
      Beep(200, 50); // params ignored in Win9X
   }
   if (yc <= arena.top || arena.bottom <= yc)
   {
      dy *= -1; // change y direction
      Beep(400, 50); // params ignored in Win9X
   }

Finally, Update() returns true. This will cause the Run() function to call Update() again:

   return true; // call me again
} // Update

The implementation of Ball is complete. Now we turn our attention to the view class.

8. Add a CArray<> of Ball pointers to the CBounceView class. Of course we will need to include Ball.h and <afxtempl.h>, the template declarations, to the top of BounceView.h.

Here is a partial listing of BounceView.h:

#include "Ball.h"
#include <afxtempl.h>

class CBounceView : public CView
{
   CArray<Ball*, Ball*> balls;
   // etc.
};

The view constructor seeds the random number generator used by the Ball constructor.

9. In the CBounceView constructor, set the size of the ball array to zero and seed the random number generator with the system clock.

Here is the code:

CBounceView::CBounceView()
{
   balls.SetSize(0);
   srand(time(0));
}

After changing brushes, the view's OnDraw() function simply traverses the ball array, drawing a circle representing each ball.

10. Implement OnDraw() so that it draws a circle corresponding to each ball in the ball array.

Here is a listing of OnDraw():

void CBounceView::OnDraw(CDC* pDC)
{
   CBounceDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   CBrush *oldBrush, *newBrush;
   newBrush = new CBrush(RGB(RED, GREEN, BLUE));
   oldBrush = pDC->SelectObject(newBrush);
   for(int i = 0; i < balls.GetSize(); i++)
   {
      int xc = balls[i]->GetXC();
      int yc = balls[i]->GetYC();
      pDC->Ellipse(xc, yc, xc + DIAM, yc + DIAM);
   }
   pDC->SelectObject(oldBrush);
   delete newBrush;
}

To complete the project, we need to use the Class Wizard to add mouse and keyboard handlers to the view class.

11. Use the [View]/[Class Wizard] to add ON_WM_CHAR and ON_WM_LBUTTONDOWN message handlers to the CBounceView class.

The left mouse button handler creates a new ball, adds it to the array, then starts it:

void CBounceView::OnLButtonDown(UINT nFlags, CPoint point)
{

   Ball* ball = new Ball(point.x, point.y);
   balls.Add(ball);
   ball->Start();
   CView::OnLButtonDown(nFlags, point);
}

The keyboard handler suspends each ball in the array if the [s] key is pressed, resumes each ball in the array if the [r] key is pressed, and stops each ball in the array if the [k] key is pressed. In this case the array is emptied and the view is invalidated. The effect will be that all of the balls will disappear from the view window.

void CBounceView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
   int i = 0, size = balls.GetSize();
   if (nChar == 's') // suspend all
      for( ; i < size; i++)
         balls[i]->SuspendThread();
   if (nChar == 'r') // resume all
      for( ; i < size; i++)
         balls[i]->ResumeThread();
   if (nChar == 'k') // kill all
   {
      for( ; i < size; i++)
         balls[i]->Stop();
      balls.RemoveAll();
      Invalidate();
   }
   CView::OnChar(nChar, nRepCnt, nFlags);
}

Build and test the application.

Producer-Consumer Problems

Bounce is a fairly simple example of a multi-threaded application, because the threads that control the bouncing balls don't need to communicate with each other. In more complicated problems threads do need to communicate with each other.

User interface threads can communicate with each other by message passing: thread A creates a message an places it in the message queue of thread B:

threadB->PostThreadMessage(WM_HELLO, x, y);

Message passing isn't very efficient, the message must be dispatched to a handler function by the receiver thread, and only small amounts of information-the integers x and y-- can be packed into the message.

A more efficient method of communication is through shared memory. This is particularly easy to set up in the Master-Slave architecture because all slaves share their master's heap and static memory segments. In this case thread A simply makes a modification to some global data structure. For example, the data structure might represent a mailbox where messages can be stored. Later, thread B simply examines the global data structure to see what thread A has done. For example, thread B might remove the message from the mailbox and read it.

But there is a problem. Suppose thread B checks the mailbox before thread A has had a chance to put the message inside? While we can predict the execution order of instructions within a single thread, there is no way in general to predict the execution order for instructions executed by different threads. If thread B misses the message, this could change the behavior of the entire application. This may even result in an error!

This type of problem is particularly difficult for programmers who feel the need to control every aspect of their program's behavior. To solve problems like these programmers use synchronization mechanisms provided by the operating system. A synchronization mechanism allows one thread to lock the global data structure until it is finished. Other threads must wait for the data structure to be unlocked before they can access it.

Producer-Consumer problems are a family of similar problems that are traditionally used to demonstrate synchronization problems and solutions. Generally speaking, a master thread creates a global buffer and two slave threads. One slave is called the producer. The producer perpetually creates imaginary objects called widgets and places them in the global buffer. The other slave is called the consumer. The consumer repeatedly removes widgets from the buffer and consumes them:

Producer-Consumer present several synchronization problems. For example, if the slaves aren't clever, the consumer slave may consume the last widget in the buffer and suspend itself, waiting for the producer to produce more widgets. At the same time, the producer slave adds a second widget to the buffer, failing to realize that the first widget is in the process of being consumed. Since the producer only notifies the consumer when it adds a widget to the empty buffer, no notification is sent. The producer proceeds to fill the buffer with widgets. When the buffer is full, the producer suspends itself, waiting for the consumer to consume some widgets to make more room in the buffer. Of course at this point both the producer and consumer are suspended!

Example: A Joint Checking Account Simulation

A joint checking account is a simple example of a producer-consumer problem. Our simulation will run as a Win32 console application. Consumer and producer worker threads will be equipped with pointers to a shared checking account (the buffer). The producer repeatedly deposits money in the joint account, while the consumer repeatedly withdraws money. (We won't name names.)

Because our simulation runs in a console window, we won't need the MFC App Wizard to create the project.

1. Create a Win32 Application Project called PC (Producer-Consumer). In the [Step 1 of 1] dialog select "A Simple Win32 Application" radio button.

We were careful not to create any dependencies in the WorkerThread class on the Bounce project. Therefore, we should be able to reuse these files in the PC project without any modifications.

2. Using the [Project]/[Add to project]/[file] menu item to add the WorkerThread.h and WorkerThread.cpp files created in the Bounce project to the PC project.

An account full of money plays the part of a buffer of full of widgets in our producer-consumer simulation.

3. Use the [Insert]/[New Class ...] menu item to add a generic class called Account to the PC project.

Our initial implementation of Account is straight forward. The current balance in the account is stored in a private member variable that can only be altered by Withdraw() and Deposit() member functions.

4. Implement the Account class.

Here is a listing of the class declaration from Account.h:

class Account
{
public:
   Account(double bal = 0) { balance = bal; }
   virtual ~Account() {}
   void Deposit(double amt);
   void Withdraw(double amt);
private:
   double balance;
};

The Withdraw() function copies the balance member variable into a local variable called temp. After decrementing temp by the amount to be withdraw, the calling thread sleeps for 10 milliseconds to simulate consumption time. Upon waking, temp is copied back to balance if it is non-negative:

void Account::Withdraw(double amt)
{
   cout << "... Withdrawing $" << amt << endl;
   double temp = balance - amt;
   Sleep(10); // simulate consumption time
   if (0 <= temp)
      balance = temp;
   else
      cout << "... sorry, insufficient funds\n";
   cout << "... exiting Withdraw(), balance = $";
   cout << balance << endl;
}

The Deposit() function also copies the balance member variable to a local variable called temp. After incrementing temp by the required amount, the calling thread sleeps for 30 milliseconds to simulate production time. (Making it always takes longer than spending it.) Upon awaking, temp is copied back to balance:

void Account::Deposit(double amt)
{
   cout << "Depositing $" << amt << endl;
   double temp = balance + amt;
   Sleep(30); // simulate production time
   balance = temp;
   cout << "exiting Deposit(), balance = $" << balance << endl;
}

Of course we will need to include <iostream>, which contains the definition of cout, at the top if PCView.h. Remember, the standard library is contained in the std namespace. Unless we want to replace all occurrences of cout by std::cout, we must formally declare that we are using the std namespace:

#include <iostream>
using namespace std;

The Account implementation is complete, for now. Next, we turn our attention to the Producer and Consumer classes.

5. Use the [Insert]/[New Class ...] menu item to add generic classes called Producer and Consumer to the PC project. Both classes should be derived from the WorkerThread class. Include Account.h in Producer.h and Consumer.h.

The Producer class encapsulates a pointer to a shared account and a cycle counter. A constructor initializes these variables. The Update() function decrements the cycle counter. If the counter is non-negative, the producer deposits $10 into the joint account and returns true, otherwise false is returned, which will cause the calling thread to terminate.

6. Implement the Producer class.

Here is a listing of the Producer class. Note that if the default cycle counter is used, the producer will deposit $10 five times, for a total of $50:

class Producer : public WorkerThread
{
public:
   Producer(Account* acct = 0, int cycles = 5)
   {
      account = acct;
      counter = cycles;
   }
   virtual ~Producer() {}
   bool Update()
   {
      if (--counter < 0) return false; // terminate
      account->Deposit(10);
      return true; // iterate
   }
private:
   int counter; // = # of deposits
   Account* account; // = the joint account
};

The Consumer class mirrors the Producer class, only the Update() function decrements the cycle counter, and if it isn't negative, withdraws $8 from the shared account.

7. Implement the Consumer class.

Here is a listing of the Consumer declaration. Note that if the default cycle counter is used, the consumer will withdraw $8 from the account at most five times (sometimes there may be insufficient funds). Thus, the total amount withdrawn will be less than or equal to $40:

class Consumer : public WorkerThread
{
public:
   Consumer(Account* acct = 0, int cycles = 5)
   {
      account = acct;
      counter = cycles;
   }
   virtual ~Consumer() {}
   bool Update()
   {
      if (--counter < 0) return false; // terminate
      account->Withdraw(8);
      return true; // iterate
   }
private:
   int counter; // = # of withdrawals
   Account* account; // = the joint account
};

If we think of Producer and Consumer threads as slaves, then the thread executing main() can be regarded as the master thread. If support code was generated for the project by a Wizard, then main() is called _tmain() and is defined in a file called pc.cpp. After including "Consumer.h" and "Producer.h" in pc.cpp, add lines to main() that create an account containing $100. Create Withdraw and Producer threads that share the account, then start both threads.

8. Modify _tmain() to create an account, then create and start a producer, and a consumer.

Here are some sample lines that can be placed in main(). Notice that at the end we read a single character from standard input, cin. This will cause the master thread to block until the user presses the return key, which will allow the producer and consumer enough time to run to completion. (Slaves are automatically terminated when their master terminates).

Account* acct = new Account(100);
Producer* Depositor = new Producer(acct);
Consumer* Withdrawer = new Consumer(acct);
Withdrawer->Start();
Depositor->Start();
cin.get(); // block until slaves are done

We are now ready to build and test the application.

9. Build and test the application.

Here is the output produced by one run of the application:

... Withdrawing $8
Depositing $10
... exiting Withdraw(), balance = $92
exiting Deposit(), balance = $110
... Withdrawing $8
... exiting Withdraw(), balance = $102
Depositing $10
... Withdrawing $8
exiting Deposit(), balance = $112
... exiting Withdraw(), balance = $94
Depositing $10
... Withdrawing $8
... exiting Withdraw(), balance = $86
exiting Deposit(), balance = $104
... Withdrawing $8
... exiting Withdraw(), balance = $96
Depositing $10
exiting Deposit(), balance = $106
Depositing $10
exiting Deposit(), balance = $116

Synchronization

But wait, something is wrong. The "insufficient funds" message never appears, so the consumer withdrew $8 five times; a total of $40. Of course the producer deposits $10 five times for a total of $50. There was $100 in the account initially, so the closing balance should have been $100 + $50 - $40 = $110, not $116. What happened?

Things went wrong at the very start of the run:

... Withdrawing $8
Depositing $10
... exiting Withdraw(), balance = $92
exiting Deposit(), balance = $110

The consumer starts to withdraw $8. In the middle of the transaction, the producer interrupts and begins a $10 deposit. Now the consumer interrupts the producer to complete its transaction, leaving a balance of $92 in the shared account. But it's too late. The producer has already copied the $100 balance into a local variable and incremented it to $110. When it regains control of the CPU, it copies its local variable back into the balance member variable of the shared account, over writing the $92 balance.

We can represent this situation using a sequence diagram:

Indivisibility

Readers might think that the root of the problem is the leisurely pace of the Account's Deposit() and Withdraw() member functions. Perhaps if we reduced these functions to single lines we could have avoided the interruption problem:

void Account::Deposit(double amt) { balance += amt; }
void Account::Withdraw(double amt) { balance -= amt; }

This idea appears to work until we set the producer and consumer cycle counters to a large value, say 30,000, then, eventually, the problem reappears. The real problem is that while an assembly language instruction may be indivisible-i.e., the CPU will complete execution of an assembly language instruction without interruption-- the same is not true for a C++ instruction. Even the simple C++ instruction:

balance += amt;

may translate into several assembly language instructions:

mov reg1, balance      // register1 = balance
mov reg2, amt         // register2 = amt
add reg1, reg2         // register1 += register2
mov balance, reg1      // balance = register1

Eventually this sequence will be interrupted by the consumer thread sometime after the first instruction but before the last. When that happens, the amount withdrawn will be lost.

Locks and Latches

One way to coordinate access to a shared resource like a joint bank account is to associate a "latch" with the resource. Like a latch on a door, a client can attach a lock to a resource latch. Locking the lock prevents other clients from accessing the resource until the lock is removed from the latch.

Locks and latches are difficult for programmers to build from scratch. Consider some of the requirements. First, locking a lock must be an indivisible operation. Otherwise, we will simply transfer the problem of simultaneous access from the locked resource to the lock itself. Second, when a client attempts to lock a latch that is already locked, the client must be automatically suspended until the latch is unlocked. Of course there may be many suspended clients waiting to access the shared resource. These clients must be stored in a wait queue associated with the latch. Finally, when a client unlocks a latch, one of two things must happen. If there are no suspended clients waiting in the queue associated with the latch, then the lock is simply removed from the latch. If there are clients in the wait queue, then the latch is left locked, and the first member of the queue is resumed.

Like windows and threads, most operating systems provide locks and latches as system resources. (Clearly the operating system is in a better position than an ordinary application to implement the special requirements described earlier.)

The Windows operating systems provide four types of latches: semaphores, mutexes, critical sections, and events; and two types of locks: single locks and multi-locks. A multi-lock can lock several latches at the same time, while a single lock locks a single latch. We won't need or discuss multi-locks.

The main difference between a critical section and a mutex, is that a mutex (like semaphores and events) can latch a resource shared by clients running inside different programs. The main difference between a mutex and a semaphore is that a semaphore allows several clients to simultaneously access a shared resource, not just one. (Of course the programmer specifies the maximum number of clients that can simultaneously access the resource.) Events are also similar to mutexes, only they can time out, releasing all of the clients on their wait queues.

Critical Sections

A critical section is an instance of MFC's CCriticalSection class. Of course instances of this class are simply wrappers for system-level critical sections. Let's associate a critical section with each instance of our Account class.

10. Add a CCriticalSection member variable to the Account class. You will need to include the <afxmt.h> header file near the top of Account.h.

Here is a listing of the Account class with an associated latch:

class Account
{
public:
   Account(double bal = 0) { balance = bal; }
   virtual ~Account() {}
   void Deposit(double amt);
   void Withdraw(double amt);
private:
   double balance;
   CCriticalSection latch;
};

Next, we must modify the Deposit() and Withdarw() methods so that they lock the latch upon entry, and unlock it upon exit.

11. Create a lock associated with the latch and lock it at the beginning of the Withdraw() and Deposit() member functions. Unlock the lock at the end of these functions.

Here are listings of the new Deposit() and Withdraw() functions:

void Account::Deposit(double amt)
{
   CSingleLock lock(&latch);
lock.Lock();
   // as before ...
   lock.Unlock();
}

void Account::Withdraw(double amt)
{
   CSingleLock lock(&latch);
lock.Lock();
   // as before ...
   lock.Unlock();
}

In fact, locks are automatically unlocked when they are destroyed. Since our locks are local variables inside Deposit() and Withdraw(), they will be destroyed when these functions terminate. Therefore, it is unnecessary to unlock them at the end. Furthermore, it is possible to create these locks in their locked state by specifying true as a second constructor argument. Here are trimmer versions of the Acccount member functions:

void Account::Deposit(double amt)
{
   CSingleLock lock(&latch, true);
// as before ...
}

void Account::Withdraw(double amt)
{
   CSingleLock lock(&latch, true);
// as before ...
}

Account instances are now thread safe.

12. Build and test the PC application.

Here is the output produced by a sample run of our program. Notice that the producer never interrupts the consumer and vice-versa. Also notice that in the closing balance is $110:

... Withdrawing $8
... exiting Withdraw(), balance = $92
Depositing $10
exiting Deposit(), balance = $102
... Withdrawing $8
... exiting Withdraw(), balance = $94
Depositing $10
exiting Deposit(), balance = $104
... Withdrawing $8
... exiting Withdraw(), balance = $96
Depositing $10
exiting Deposit(), balance = $106
... Withdrawing $8
... exiting Withdraw(), balance = $98
Depositing $10
exiting Deposit(), balance = $108
... Withdrawing $8
... exiting Withdraw(), balance = $100
Depositing $10
exiting Deposit(), balance = $110

Mutexes

We can re-declare latch as an instance of MFC's CMutex class. No other changes need to be made to the code:

class Account
{
public:
   Account(double bal = 0) { balance = bal; }
   virtual ~Account() {}
   void Deposit(double amt);
   void Withdraw(double amt);
private:
   double balance;
   CMutex latch;
};

Although we don't use this feature in our example, a mutex can be given a name that can be used by clients in other applications that wish to access the same resource.

Semaphores

Semaphores also have system-wide identities. A semaphore encapsulates three numbers: maximum capacity, current capacity, and current count. The maximum capacity is the maximum number of clients that may access the associated resource. The current count is the number of clients that currently have access to the associated resource. The current capacity is the maximum capacity minus the current count:

CURRENT_CAPACITY = MAX_CAPACITY - CURRENT_COUNT

When a semaphore is created, its maximum and current capacities are specified as constructor arguments:

CSemaphore(CURRENT_CAPACITY, MAX_CAPACITY);

The default value of both parameters is one. In other words, only one client may access the associated resource at a time, and there are currently no clients accessing the resource. Since only one client may access our joint bank account at a time, we can simply re-declare latch as a CSemaphore:

class Account
{
public:
   Account(double bal = 0) { balance = bal; }
   virtual ~Account() {}
   void Deposit(double amt);
   void Withdraw(double amt);
private:
   double balance;
   CSemaphore latch;
};

Events

Another approach to synchronization is to use events rather than latches. An event is an object that can be in one of two states: signaled (the event has occurred) or non-signaled (the event has yet to occur).

MFC events are instances of the CEvent class. If a client thread attempts to lock a non-signaled event, it is automatically suspended and moved to a wait queue associated with the event. An event can be signaled by calling its SetEvent() member function. An automatic event-the default case-- returns to its non-signaled state as soon as the first client thread suspended on its associated wait queue resumes execution. A manual event remains in its signaled state until its ResetEvent() function is called:

CSignalEvent e(false, true); // e = non-signaled & manual
e.SetEvent(); // e.state = SIGNALED & release 1st waiter
e.ResetEvent(); // e.state = NON_SIGNALED

We can simply re-declare latch to be a CEvent. By default, all events are created in their non-signaled state, so we will need to add an initializer list to the Account constructor that initializes latch in its signaled state:

class Account
{
public:
   Account(double bal = 0): latch(true) { balance = bal; }
   virtual ~Account() {}
   void Deposit(double amt);
   void Withdraw(double amt);
private:
   double balance;
   CEvent latch;
};

As before, the Deposit() and Withdraw() functions begin with an attempt to lock the latch. In addition, these functions must also signal the event upon exit:

void Account::Deposit(double amt)
{
   CSingleLock lock(&latch, true);
// as before ...
   latch.SetEvent(); // latch.state = SIGNALED
}

void Account::Withdraw(double amt)
{
   CSingleLock lock(&latch, true);
// as before ...
   latch.SetEvent(); // latch.state = SIGNALED
}

Like semaphores and mutexes, events can have system-wide identity. In addition, clients can use the global WaitForSingleObjects() function to block themselves on an event until the event occurs, or until a specified interval of time elapses:

void Account::Deposit(double amt)
{
   WaitForSingleObject(latch, INFINITE);
// as before ...
   latch.SetEvent();
}

Problems

Problem 5.1

Build and test the Bounce application.

Problem 5.2

Create a WorkerThread wrapper class. Test your implementation by re-implementing the Bounce application.

Problem 5.3

Modify Bounce so that a modeless dialog box allows users to set the color, speed, and state (pause, resume, stop) of each ball. (Balls are specified by their array indices.)

Problem 5.4

Modify bounce so that balls perform "collision detection". That is, when the bounding boxes of two balls intersect, then both balls reverse the signs of their dx and dy member variables and make a colliding beep.

Problem 5.5

Build and test the PC (Producer-Consumer) project. Experiment with multiple producers and consumers.

Problem 5.6: Interlocked Functions

In the PC project, experiment with the "single-line" implementations of Account::Deposit() and Account::Withdraw():

void Account::Deposit(double amt)
{
   cout << "Depositing $" << amt << endl;
   balance += amt;
   cout << "Balance = $" << balance << endl;
}

void Account::Withdraw(double amt)
{
   cout << "...Withdrawing $" << amt << endl;
   balance += -amt;
   cout << "...Balance = $" << balance << endl;
}

How many cycles elapse until the deposits and withdrawals don't add up?

We can actually make this idea work if we use one of the Windows platform's interlocked functions. An interlocked function disables interrupts upon entry and enables interrupts upon exit. For example, if all monetary amounts are represented as long integers rather than doubles, we can use the InterlockedExchangeAdd() function to increment and decrement balance:

void Account::Deposit(long amt)
{
   cout << "Depositing $" << amt << endl;
   InterlockedExchangeAdd(&balance, amt);
   cout << "Balance = $" << balance << endl;
}

void Account::Withdraw(long amt)
{
   cout << "...Withdrawing $" << amt << endl;
   InterlockedExchangeAdd(&balance, -amt);
   cout << "...Balance = $" << balance << endl;
}

Experiment with this approach. Allow your simulation to run for several hours. Do the deposits and withdrawals add up?

Problem 5.7: Readers and Writers

Create a producer consumer simulation in which the producer writes strings to a file (the buffer) while the producer reads strings from the file.

Problem 5.8: Predators and Prey

In a predator-prey game predators-ghosts, which appear as blue circles or bitmaps-- pursue a prey-- the hero, who appears as a yellow circle or bitmap-- around the view window. Each ghost is controlled by a worker thread. The Update() function of a ghost worker thread locates the hero, then moves the ghost several steps in the hero's direction. The number of steps is determined by the ghost's speed attribute.

The hero is controlled by the application's main thread, which listens for the user to press the arrow keys. Pressing an arrow key moves the hero a single step in the direction of the arrow. When the bounding box of a ghost intersects the bounding box of a hero, the ghost "eats" the hero and the game ends. Unlike ghosts, the hero can wrap around the screen. In other words, if the hero moves off the bottom of the view window, he reappears at the top of the view window, and all ghosts will need to change direction.

A [Game] menu allows the user to set the number and speed of the ghosts, and to start the game.

Problem 5.9: Dining Philosophers

Dining Philosophers is another classic example of a synchronization problem. Plato, Buddha, Spinoza, Kant, and Hume sit at a circular table in a Chinese restaurant. Each has a plate of food in front of him. Between each pair of plates is a single chopstick. Plato grabs the chopsticks on either side of his plate and begins eating. Of course Plato's neighbors, Buddha and Hume, must wait for Plato to release the chopsticks so they can begin eating (a philosopher must acquire the chopsticks on both sides of his plate before he can eat). If philosophers are worker threads, then the Update() function might look something like this:

bool Philosopher::Update()
{
   lock left chopstick;
   lock right chopstick;
   numBites++; // eat
   unlock left chopstick;
   unlock right chopstick;
   return true; // go "think" for a while
}

The user interface is a form view containing five edit boxes that shows the number of bites each philosopher has taken:

The first goal of the program is to guarantee that no philosopher starves. The second goal is to guarantee that each philosopher gets approximately the same amount to eat. For example, suppose at the beginning of the program each philosopher grabs the chopstick to his left, then waits for the chopstick on his right. Of course Buddha's right chopstick is Spinoza's left chopstick, which Spinoza holds and won't release until he acquires Kant's left chopstick. The result is deadlock and all five philosophers starve. It's also possible for two or three of the philosophers to eat, while the remaining philosophers starve.

One solution is to have the master thread play the role of a moderator who schedules the chopsticks for the philosophers.

Problem 5.10: Marquee

Create a single document application called Marquee. Marquee is a form view application that scrolls a string in an edit box. The form view also has [Pause] and [Resume] buttons that pause and resume the scrolling. User can type new strings into the edit box. This application requires a worker thread to scroll the string, and a user interface thread to listen for button clicks.