CSCI 4630
Summer 2001
Programming assignment 2

Due: Monday, June 4.

The assignment

This assignment is to implement a simple producer/consumer program using POSIX threads. You are to design the threads to run with a preemptive scheduler, and to communicate through a bounded buffer as shown in the book. Design the buffer to have about 10 slots. Use the implementation shown in the text, using an array to hold the contents of the buffer, and counters to hold the number of occupied cells, the next location for the producer to insert a value and the next location for the consumer to remove a value. Do not forget to initialize the counters. Also, be sure to update the counters in the correct order. The book solution uses the wrong order.

The producer should send a sequence of integers to the consumer. Specifically, it should send the sequence 1, 2, 3, ..., 1000000, in that order. The consumer should receive them and check that it has indeed received exactly that sequence, in that order.

Implement the buffer in three ways.

  1. Implement the buffer in a way that is not thread-safe, not protecting the update of the counter.

  2. Implement the buffer using a semaphore to protect the critical section where the shared counter is updated. The threads will need to wait to enter the critical section, but will busy-wait by themselves. That is, the producer will busy-wait when the buffer is full, and the consumer will busy-wait when the buffer is empty. Do not try to avoid the busy-waiting.

  3. Implement the shared counter as a wait-free shared counter, as described in class. Do not use any semaphores for this one. Again, the threads will busy-wait.

Run the program using all three implementations of the shared bounded buffer. In the first implementation you should expect that something will go wrong. The consumer should discover that the numbers received are not in the correct sequence. As soon as the consumer sees an error, it should report how much of the sequence was received correctly, and what the next (incorrect) number was.

The second and third implementations of the bounded buffer should work correctly. Test them to make sure. The consumer should report that all went well.

Turn in all three solutions. (So don't destroy one to build the next one. Make a copy instead.)

POSIX threads

This describes only the most basic aspects of how to create and manage POSIX threads. It is assumed that you are writing your program in C or C++, and using a POSIX compliant system. (Solaris 2.x and Windows NT 4.0 both have POSIX interfaces.) I have tested this under Solaris 2.x. The lab machines are running Solaris 8, which is really Solaris 2.8 (which is really really SunOS 5.8 -- make up your mind!).

To create threads, you start by creating a structure that describes desired thread attributes. You can then use that structure to create many threads with those attributes. Create a variable attr of type pthread_attr_t, and initialize it as follows.

      pthread_attr_init(&attr);
      pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
The first line installs the default attributes. The second line indicates that the thread manager should switch between threads in a way similar to the way the system's process manager switches between processes. That is, preemptive scheduling of the threads is used. (The default is to use cooperative multiprogramming among the threads.)

Then create the thread as follows.

      status = pthread_create(&tid, &attr, func, NULL);
where
  1. tid is a variable of type pthread_t (typically another name for unsigned int). It will be set to the thread id, an integer.

  2. func is a function that the thread will run. The prototype for func should be

          void* func(void*);
    
    (You don't have to call the function func. You can call it any name that you like. But it must take a parameter of type void*, and produce a result of type void*.) Make func return NULL.

  3. status is a variable of type int. Function pthread_create returns 0 if the thread was created, and a nonzero value if there was an error that prevented creation of the thread.

  4. The last parameter of pthread_create is the parameter that is passed to func when the thread starts. It has type void*. So, when pthread_create(tid,func,a,p) starts, it runs f(p). The line above shows this parameter set to NULL.

When the thread is created it starts running immediately.

You will need to create two threads, and then wait for the threads to terminate. (If you don't wait, and you let the main program terminate, the threads that it created will be destroyed prematurely.) Function pthread_join allows one thread to wait for another one. Use

      pthread_join(tid,NULL);
to wait for thread number tid to terminate before continuing. Since you will have created two threads, you will have to wait for both of them. You will have to wait for them in a specific order. It does not matter which one finishes first. A terminated thread waits until another thread joins with it before it disappears. (It becomes a zombie thread.)


Compiling and Linking

You will need to include header file pthread.h to use POSIX threads. Also, when you link your program, you will need to use the pthread library. If you compile using gcc, you write

   gcc myprog.c -lpthread

It is very important that you use the pthread library. If you forget to use it, a default pthread_create function will be provided for you that does nothing at all. You will wonder why your program is not doing anything.

When using threads, it is a good idea to include directive

#define _REENTRANT
to force the compiler to generate reentrant code. (Most compilers do this anyway.) Reentrant code has a read-only TEXT segment (where the program is stored) so that it can be run simultaneously by more than one thread.


Semaphores

You can create a POSIX semaphore that can synchronize threads or processes. There are unnamed versions (accessible only to programs that know about them) and named versions (accessible to programs via the file system). Here, I describe only unnamed versions.

Include header file semaphore.h to use POSIX semaphores. You can create an unnamed semaphore that can synchronize threads that belong to the same process, with an initial value of 1, as follows.
     sem_t sem;
     sem_init(&sem, 0, 1);
Now use
     sem_wait(&sem);
to do a wait (or P) on semaphore sem, and
     sem_post(&sem);
to do a signal (or V) on sem. When you are finished with the semaphore, destroy it using
     sem_destroy(&sem);