Project 2
Citius, Altius, Fortius
|
Due: Monday, October 9, 5:00 PM
1. Memory Allocation
1.1 Quick Overview
In this project, you will build a simulation of a gymnastics competition that will allow you to live the
excitement of the Olympic Games in the comfort
of your room, all without having to listen to Bob Costas. At
the end of this project you will gain a good understanding of the following
issues:
-
How to create and manipulate threads.
-
How to use synchronization variables, including mutexes and condition variables
The output of this project should be a software artifact and a proof that
it works. You will need to write up a detailed description of your
data structures, and the synchronization mechanisms that you have used
in the implementation.
1.2 Working with
Threads
There are several flavors of threads. Threads could be implemented by the
application with no support from the operating system beyond that available
to conventional heavy weight processes. Such threads are commonly called
user-level threads. An implementation may support cooperative threads or
pre-emptible threads. Cooperative threads run and explicitly transfer control
of the CPU among themselves under program control. An example of such threads
are the ones used in Windows 3.1. A thread in such an implementation runs
until it decides to "yield" the CPU to another thread. Programming threads
in this model is conceptually easier, but bugs can be deadly. If a runaway
thread gets caught up in an infinite loop, it will not reach the point
in the program where it yields the CPU, effectively blocking the application.
User-level threads could also be implemented such that they are pre-emptible at any point during execution. The application could possibly use the
timer facility in the operating system to generate interrupts at pre-specified
times where the thread scheduler takes over. In many ways, the thread scheduler
under this model resembles the operating system scheduler.
User-level threads, however useful they are, are still "multiplexed"
over the application "true" threads. That is, the system supplies a kernel
thread to each heavyweight process in the system. That thread can create
additional kernel threads if necessary. Kernel level threads are supported
directly by the operating system.
Independent from the manner threads are implemented, a well-written
program should never make assumptions about a particular implementation.
Such a program would be written to the generic model of multithreaded programming, and would work on either type of threads.
1.3 A Simple Thread Package
To simplify your task, we have developed a simple
thread package on top of the standard pthread package for your use. The
idea is to shield you from the irrelevant detail that inevitably is part
of dealing with pthreads. This way, you use the standard package but you
also focus on the project at hand.
The files are sthread.h,
sthrea
d.cc,
and a simple program
that
demonstrates their use.
Files to be included in your .cc files: sthread.h
Files to be linked with your .o files: sthread.o
(compiled from sthread.cc)
Other required compiler options (when compiling
.cc files): -D_POSIX_PTHREAD_SEMANTICS
Other required linking options (when linking
.o files): -lpthread -lrt
The package provides threads, mutex locks, and
condition variables as well as some other utility functions that you may
need (such as a random number generator). This package is built on the
posix thread library. For more information, see the man pages for the library
functions used in the sthread.cc code.
2. Project Overview
2.1 The Model
The competition involves m teams, competing
on k different apparatus. Each team has n members. Each gymnast
must perform a valid exercise at each apparatus.
Gymnasts randomly select which apparatus they
are going to use next among the ones for which they have not yet completed
a valid exercise. Since at all times a given apparatus can be used by
at most one gymnast, there is a queue of waiting gymnasts for each apparatus.
When the apparatus becomes available, the gymnast at the head of the queue gets to perform her exercise. Before starting
the exercise, however, she picks up some chalk to strengthen her grip.
At the end of the exercise, before moving to another apparatus (if she still has
some exercises to complete) the gymnast waits to learn her score. A judge (there
is one judge for each apparatus) assigns the score and updates a scoreboard,
which keeps track of the scores of each of the gymnasts and of the overall score
of each team. A judge assigns the score by generating a random number in the
range [9.00, 10.00] (just as in the real Olympics!).
Because
of cuts in the IOC's budget after the recent scandals, the scoreboard is
not electronic: the judge has to actually go to the scoreboard and play with
huge plastic numbers. As
a result, a scoreboard update is not instantaneous, but requires a time
which is randomly and uniformly distributed in the range [0, MAX_UPDATE]
milliseconds. Because of the possibility that several outstanding
requests may be updating the scoreboard simultaneously, your solution
should use synchronization mechanisms to ensure that concurrent
updates to several scoreboard entries will execute correctly.
A gymnast cannot start her exercise on an apparatus
until the judge for that apparatus has come back from updating the scoreboard
for the previous gymnast.
A team leaves the competition area after all
of its members have completed all their exercises: a gymnast who has completed
her exercises waits for the other members of her team to do the same before she
is allowed to leave The
competition ends when all teams are done.
2.2 Managing
the Chalk
Thanks to a contract with a packaging firm that
happens to be owned by the cousin of an IOC member, chalk is now made available
to gymnasts in single-dose little bags. There is one container in the competition
area, which can hold up to c bags. Every gymnast must pick up a
chalk bag before starting her exercise. The container is refilled by a
well-meaning but rather disorganized set
of volunteers who add bags to
the container at a rate determined by a random uniform distribution
from the range [0, MAX_REFILL] milliseconds. A gymnast who finds the container
empty wait until a bag becomes available. Well-meaning volunteers cannot add
bags to a full container, and must wait until space becomes available.
2.3 The Scoreboard
The scoreboard contains the status of the competition,
including the scores of each gymnast, and the overall score of each team.
We need to design a data structure that allows safe and efficient concurrent
access to the scoreboard. At the simplest level, the scoreboard could
be protected by a single mutex that must be acquired before the database
could be accessed. Obviously, this simple solution would be detrimental
to performance. Your solution must therefore protect the scoreboard
entries at the individual level. That is, different accesses to different
scoreboard entries must be able to proceed concurrently. This means that
there has to be a mutex for each scoreboard entry. You may also need
other mutexes to protect resources that different entries share (e.g.,
a team's overall score).
2.4 Extracredit: Repeating exercises
For those of you who want their simulation to be truly realistic, we will assume that the judges
periodically discover that some apparatus has been set incorrectly. In
particular, we will assume that the inter-arrival time of these discoveries
follows a random, uniform distribution from the range [0, MAX_GOOFUP] milliseconds.
Departing form reality, we will assume that an apparatus can be set incorrectly
at most once. If an apparatus is set incorrectly, all the gymnasts that
have performed their exercise with the badly set apparatus and have
not yet left the competition area must repeat their exercise. As a result,
the scoreboard must be updated by removing
all the entries corresponding to the invalid exercises, including the overall
score of the affected teams.
NOTE: This is extracredit. You will receive full credit in the project
just by doing well the rest of the simulation. Think about the extracredit only
after you are satisfied with the rest of your solution.
2.3 Your Solution
You will simulate each gymnast, judge, and well-meaning
volunteer using a thread. For each apparatus,
you'll have to implement a FIFO queue that allows concurrent insertion
at the end of the queue, and removal from the head of the queue. Additionally,
condition variables must be used to signal when an apparatus has become
available, when the chalk container is full, and when it is empty. You
will need to use mutexes to protect access to the scoreboard.
During initialization, the root thread of your
program forks off k threads for the judges,
one thread for each of the s well-meaning volunteers, and one thread for each gymnast. It then waits for the simulation to complete,
and declares the medal winners.
When a gymnast thread is created, her team is chosen
randomly, but making sure that eventually every team has n members.
Your program will take the following arguments
olympia n m k c s MAX_EXERCISE
MAX_UPDATE MAX_REFILL
m: The total number of teams.
n: The number of gymnasts per team.
k: The number of distinct apparati (and of judges).
c: The number of chalk bags in the container.
s: The number of chalk suppliers.
MAX_EXERCISE: Helps generate the time taken
to perform each exercise (in units of milliseconds)
MAX_UPDATE: Helps generate times
for scoreboard updates (in units of milliseconds)
MAX_REFILL: Helps generate the chalk bags
inter-arrival time. (in units of milliseconds)
If you are going for the extracredit, you'll need to add an extra parameter, MAX_GOOFUP
where
MAX_GOOFUP: Helps generate the time at which a judgediscovers
an error in his apparatus setup (in units of milliseconds).
You will use the sthread_sleep call in the sthread
library to generate the necessary delays within the simulator. For example,
the time required to complete an exercise can be simulated by having the
appropriate thread pause the right amount of time; a similar idea can be
used to simulate the time necessary to update the scoreboard.
3. Structure of your solution
The only way to keep the simulation under control is to
structure it in a modular fashion. For each of the main data
structures in the simulation (the scoreboard, the chalk container, the queues of
each apparatus, the queue on which gymnasts wait for their scores) you should
define a corresponding object as well as the methods for manipulating it. For
example, shared state should be hidden behind object interfaces, and each object
should synchronize the multiple threads accessing its methods. Thus, a thread
code should never access a shared state variable directly, nor call directly lock()
or unlock(), or call directly signal() or wait(): all of these actions should be
performed by invoking a high-level method defined on the object. Thus, the main
loop for each type of thread should be very simple and readable.
3.1 The Report
You will need to describe the data structures and
synchronization strategy that you use for your project. We
suggest that you write most of this report before you write the
code for the simulation. The point is: you should think about your
overall synchronization strategy before you get deep into writing code
and lose track of the big picture. Additionally, you need to describe
the synchronization primitives that you have used.
In particular, we look for the following:
-
A description of the major data structures in the
project and how they relate
-
A proof that your queuing mechanism works (especially
in boundary condition situations) and that it provides both safety and
liveness.
-
A proof that the scoreboard works and provides
both safety and liveness (be particularly careful to prove that it is not
subject to deadlocks!)
-
A proof that the program terminates properly
-
An explanation of how your solution maximizes
concurrency so that multiple simultaneous requests can access different entries
in the scoreboard without waiting for each other excessively.
For each "proof," we are just looking for a relatively
informal argument that your solution provides both (1) safety and
(2) liveness. (E.g., that (1) the system is not subject to race
conditions and that some sensible set of invariants always holds after each
method invocation and (2)
that the system can not deadlock and that progress is guaranteedAt the end of the simulation,
you should output the final score of each team, and the time taken by each
team to complete. You should also announce the medal winners!
4. Precautions when Working with Threads
Programming multithreaded programs requires extra
care and more discipline than programming conventional programs. The reason
is that debugging multithreaded programs remains an art rather than a science,
despite more than 30 years of research. Generally, avoiding errors is likely
to be more effective than debugging them. Over the years a culture has
developed with the following guidelines in programming with threads. Adhering
to these guidelines will ease the process of producing correct programs:
1. All threads share
heap data. This requires you to use proper synchronization primitives whenever
two threads modify or read the shared data. Sometimes, it is obvious how
to do so:
; char a[1000]
; void Modify(int m, int n)
; {
; a[m + n] = a[m] + a[n];
&n bsp;
&nb sp; // ignore bound checks
; }
If two thread will be
executing this function, there is no guarantee that both will perceive
consistent values of the members of the array
a. Therefore, such a statment has to be protected by a mutex that ensures
the proper execution as follows:
; char a[1000]
; void Modify(int m, int n)
; {
; Lock(p);
; a[m + n] = a[m] + a[n];
&n bsp;
&nb sp; // ignore bound checks
; Unlock(p);
; }
where p is a synchronization
variable.
2. Beware of the hidden data structures!
While your own variables on the heap must be protected, it is also necessary
to ensure that data structures belonging to the libraries and runtime system
also be protected. These data structures are allocated on the heap, but
you do not see them. Access to library functions can create situations
where two threads may corrupt the data structures because proper synchronization is lacking. For example, two threads calling the memory allocator simultaneously through the malloc() library call might create problems if the data structures
of malloc() are not designed for concurrent access. In that case,
data structures used to track the free space might be corrupted. The solution
is to use a thread-safe version of libc.
For this reason, it is important to compile your
program with the -D_POSIX_PTHREAD_SEMANTICS flag, so that the compiler
knows to use the "thread safe" (synchronized) versions of libraries.
Note that not all libraries offer thread-safe
versions. Some library calls may only
be used with single threaded programs. Before using a library call, you
should find out whether it supports multithreaded use. In Solaris, you
do this by looking at the man page for that library function. The man page
will indicate that the call is "Thread Safe" (allows concurrent access
by different threads assuming you have compiled with -D_POSIX_PTHREAD_SEMANTICS
), "Safe" (uses a lock to avoid concurrency within the call so that multithreaded
programs may safely use it at some performance cost), and "Unsafe" (you
can't use the call from a multi-threaded program).
3. Simplicity of the code is also an important
factor in ensuring correct operation. Complex pointer manipulations
may lead to errors and a runaway pointer may start corrupt the stacks of
various threads, and therefore manifesting its presence through a set of
incomprehensible bugs. Contrived logic, and multiple recursions may cause
the stacks to overflow. Modern computer languages such as Java eliminate
pointers altogether, and perform the memory allocation and deallocation
by automatic memory management and garbage collection techniques. They
simplify the process of writing programs in general, and multithreaded
programs in particular. Still, without understanding of all the pittfalls
that come with programming multithreaded applications, even the most sophistica
ted programmer using the most sophisticated language may fall prey to some
truly strange bugs.
5. On Programming and Logistics
The following guidelines should help smooth the process
of delivering your project. You can help us a great deal by observing the
following:
-
After you finish your work, please use the turnin
utility
to submit your work.
Usage: |
turnin -submit TA-name cs372-proj2 your_files |
-
Do not include object files in your submission!! (Or core dumps!!!)
-
You must use a Makefile to compile the program
and produce an executable.
-
The project will be graded on the UltraSPARC cheese cluster (feta, etc.)
In principle, the libraries you use are posix complient, so portability
should not be a major issue if you develop on a different platform. As
usual, however, porting to Solaris is your responsibility and no extensions
will be granted while you do this.
-
Select reasonable names for your files and variables. This way, you make
grading easier.
-
You will work with a partner in this project. If you cannot form a partnership,
nform the instructor as soon as possible.
-
Your files should never refer to absolute names. For example, to include
foo.h, do not write:
#include
"/afs/.../foo.h" /* poor style */
-
You must provide a documentation file that explains briefly your solution
and any assumptions that you made. Your documentation file should not be
more than one screenful of ASCII text (24 x 80).
-
You must include a file PROOFS.txt that discusses how you addressed the
issues listed above.
-
You are encouraged to reuse your own
code that you might have developed in previous courses to handle things
such as queues, sorting, etc.
-
You are also encouraged to use code provided by a public library such as
the GNU library.
-
You are encouraged to discuss the problem with your colleagues. However,
you are not allowed to look at their code or help them in debugging.
-
If you find that the problem is underspecified, please make reasonable
assumptions and document them in the documentation file.
-
You are required to adhere to the multi-threaded coding standards/rules
discussed in class.
-
Code will be evaluated based on its correctness, clarity, and elegance.
Strive for simplicity. Think before you code.