CS 372

Fall 2000 


 
 

Project 2 

Citius, Altius, Fortius

Due: Monday, October 9, 2000, 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: 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: 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 guaranteed

At 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:
Start early, we mean it!!!