CS 1550 – Project 2: Producer/Consumer

Due: Sunday, March 2, 2014 by 11:59pm

Project Description

We’ve been learning about synchronization and solving the producer/consumer problem using semaphores. In this project, we will explore the issues of race conditions and attempt to solve them with and without the system’s help.

How it Will Work

You will make a userspace application called prodcons that will implement the producer/consumer problem using pthreads. We will be using pthread_create() to create a thread for the producer and reuse the main thread for the consumer. The buffer size is configurable from the command line.

If we run the executable as:  ./prodcons 1000, we could see something like this output:

Producer Produced: 0

Producer Produced: 1

Producer Produced: 2

Consumer Consumed: 0

Consumer Consumed: 1

Consumer Consumed: 2

 

Basically we will be producing sequential integers and then consuming them by printing them out to the screen. The correct program should run as an infinite loop and never deadlock. Both the producer and consumer share the same buffer (i.e., there is only one buffer total).

Synchronization

We covered the “right” way to solve this with either semaphores or condition variables. But first, we want to explore the problem itself.

Our implementation requires a sleep and wakeup function, which we see is synchronized with a pthread_mutex to ensure that there is not a race condition. But what if we wanted to see the race condition in action? We need to implement our own sleep and wakeup that don’t require a mutex. We can do this using signals.

We can use two special signals in Linux, SIGUSR1 and SIGUSR2, which are reserved for whatever purposes we want. We can inform the OS that our thread would like to handle these signals, and then we can wait for one to occur synchronously, forming a basic sleep function. We could then allow the other thread to send a signal and wake us up.

Let us define PROD_SIGNAL as SIGUSR1 and CONS_SIGNAL as SIGUSR2.

Sleeping

To allow a thread to sleep while waiting on the signal, we need to inform the application about these signals being non-fatal. In our main(), we can do the following:

sigset_t set;

int s;

 

sigemptyset(&set);

sigaddset(&set, PROD_SIGNAL);

sigaddset(&set, CONS_SIGNAL);

s = pthread_sigmask(SIG_BLOCK, &set, NULL);

if (s != 0)

        // deal with error

 

Then we can make our own sleep function, to call as appropriate in our threads:

void my_sleep(int who) {

        int sig;

        sigemptyset(&set);

        sigaddset(&set, who);

        sigwait(&set, &sig);

}

Waking Up

We can then use a pthread version of kill() to send a signal and make a my_wakeup function:

pthread_kill(pthread_t thread, int signal);

If you need the pthread_t of the main thread, you can always ask for it with pthread_self().

Atomicity

The pthread library conveniently provides us a mutex to use for synchronization. We also explored a user space implementation using busy waiting: Peterson’s solution.

We want to compare the condition variables and mutexes provided with a semaphore of our creation whose down() and up() are themselves protected using the enter and leave critical region operations of Peterson’s solution.

Make a semaphore from the pseudocode from class and use it to solve the producer/consumer problem.

Experiments

We now have three versions of the producer/consumer program:

  1. The unsynchronized version
  2. The version that uses our semaphore with busy waiting
  3. The version that uses pthread_mutex and pthread_condtion

We want to conduct several experiments:

First, with the unsynchronized version, run the program for a while and describe what happens. Do you see evidence of a race condition? Try various buffer sizes and also consider the effects of print statements.

Unintentionally, the semaphore with Peterson’s solution has a problem. What is this problem and why can’t we fix it? How might the implementation of pthread_condition_wait() work?

Second, compare the throughput of the two synchronized versions. We can use the Unix time utility to determine the user time of a program. Make your program produce and consume sufficient items and time how long it takes. Average several runs to make sure your number is reasonable.

To make the Peterson’s solution potentially run longer, we can restrict the true parallelism and bind our process to a single core.

Before the #includes:

     #define _GNU_SOURCE

Then also

                 #define <sched.h>

In main, do:

     cpu_set_t cpuset;

     CPU_ZERO(&cpuset);

     CPU_SET(0, &cpuset);

 

     if(sched_setaffinity(getpid(), sizeof(cpu_set_t), &cpuset) != 0)

     {

           perror("Setting affinity failed\n");

           exit(-1);

     }

 

This will bind the program to only run on core 0. You may pick another of the 4 cores since many people may be using that particular one.

Make sure that your experiment runs produce the same number of items during comparison, or normalize the runtimes to a per-item cost.

Write up your results for these questions in a document to submit along with the three source code files you have produced.

File Backups

One of the major contributions the university provides for the AFS filesystem is nightly backups. However, the /u/OSLab/ partition is not part of AFS space. Thus, any files you modify under your personal directory in /u/OSLab/ are not backed up. If there is a catastrophic disk failure, all of your work will be irrecoverably lost. As such, it is my recommendation that you:

Backup all the files you change under /u/OSLab to your ~/private/ directory frequently!

Loss of work not backed up is not grounds for an extension. You have been warned.

Requirements and Submission

You need to submit:

 

Make a tar.gz file as in the first assignment, named USERNAME-project2.tar.gz

Copy it to ~jrmst106/submit/1550 by the deadline for credit.