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.