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.