Lab 7: Solving the Producer Consumer Problem with PThreads
In this lab, we use PThreads to study the canonical Producer Consumer Problem. The solutions we examine achieve synchronization through different mechanisms: busy waiting and semaphores.

In this lab we will ensure you see how to use some shared variables to both exchange information between a producer and consumer in a queue, as well as tracking the state of the information exchange queues.

The goal of the laboratory experience is to give you an appreciation of the variations in behavior that can result from the concurrency present in a mutli-threaded application, and to give you some experience with how to control that concurrency to ensure correct computation behavior.

 

Lab Materials
  1. Slides
  2. Lab Files
  3. Discussion Video


Assignment

You need to complete the implementation of producer_consumer.c to work with an arbitrary number of producer and consumer threads. producer_consumer takes as its arguments the number of producer and consumer threads it will use. The producers (combined) should produce exactly QMAX integers, and the consumers (combined) should consume exactly QMAX integers.

The makefile as provided will build the producer_consumer executable when you type `make` and as required when you use any of the targets running tests. The test targets provided run the application with all combinations of one consumer, one producer, five consumers, and five producers. The tests produce a narrativeX.raw (X = the test # in the Makefile) file that provides the narative statements in the order produced by all the threads running concurrently. For example, this is the raw output for test 1 (one producer and one consumer) produced by our example solution code. The stater code has not concurrency control implemented, so its output will be quite different.

 
  P-1 C-1   
************
    RAW     
************
prod 0:	 0.
prod 0:	 1.
prod 0:	 2.
prod 0:	 3.
prod 0:	 4.
prod 0:	 FULL.
con 0:	 0.
con 0:	 1.
con 0:	 2.
con 0:	 3.
con 0:	 4.
prod 0:	 5.
prod 0:	 6.
prod 0:	 7.
prod 0:	 8.
prod 0:	 9.
con 0:	 5.
con 0:	 6.
con 0:	 7.
con 0:	 8.
con 0:	 9.
prod 0:	 10.
prod 0:	 11.
prod 0:	 12.
prod 0:	 13.
prod 0:	 14.
con 0:	 10.
con 0:	 11.
con 0:	 12.
con 0:	 13.
con 0:	 14.
con 0:	 EMPTY.
prod 0:	 15.
prod 0:	 16.
prod 0:	 17.
prod 0:	 18.
con 0:	 15.
con 0:	 16.
con 0:	 17.
con 0:	 18.
con 0:	 EMPTY.
prod 0:	 19.
prod 0:	 20.
prod 0:	 21.
prod 0:	 22.
con 0:	 19.
con 0:	 20.
con 0:	 21.
con 0:	 22.
con 0:	 EMPTY.
prod 0:	 23.
prod 0:	 24.
prod 0:	 25.
prod 0:	 26.
con 0:	 23.
con 0:	 24.
con 0:	 25.
con 0:	 26.
con 0:	 EMPTY.
prod 0:	 27.
prod 0:	 28.
prod 0:	 29.
prod 0:	exited
con 0:	 27.
con 0:	 28.
con 0:	 29.
con 0:	exited
      

The narrativeX.sorted (X = the test # in the Makefile) file groups the narrative statements of each thread together, but keeps them in the order produced. The raw file shows how actions of different threads are interleaved, and the sorted file shows the order in which each thread experienced execution.

The producers note when they see the queue as FULL and the consumers note when they see it as EMPTY. Note the frequency with which consecutive FULL or EMPTY statements occur in the narrative for a single thread. This shows how often a thread is unblocked only to block again. You should run each test several times to see how much behavior varies from run to run. For example, this is the sorted output from our example solution code for 5 producers and 5 consumers.

 
  P-5 C-5   
************
  Sorted    
************
con 0:	 0.
con 0:	 1.
con 0:	 2.
con 0:	 3.
con 0:	 4.
con 0:	 21.
con 0:	 22.
con 0:	 23.
con 0:	 24.
con 0:	 25.
con 0:	 26.
con 0:	 27.
con 0:	 EMPTY.
con 0:	 28.
con 0:	exited
    
con 1:	 5.
con 1:	 6.
con 1:	 7.
con 1:	 8.
con 1:	 9.
con 1:	 10.
con 1:	 11.
con 1:	 EMPTY.
con 1:	 29.
con 1:	exited
    
con 2:	 12.
con 2:	 13.
con 2:	 14.
con 2:	 15.
con 2:	 16.
con 2:	 17.
con 2:	 18.
con 2:	 EMPTY.
con 2:	 19.
con 2:	 20.
con 2:	exited
    
con 3:	exited
    
con 4:	exited
    
prod 0:	 0.
prod 0:	 1.
prod 0:	 2.
prod 0:	 3.
prod 0:	 4.
prod 0:	 FULL.
prod 0:	 FULL.
prod 0:	 FULL.
prod 0:	 FULL.
prod 0:	 8.
prod 0:	 FULL.
prod 0:	exited
    
prod 1:	 FULL.
prod 1:	 5.
prod 1:	 FULL.
prod 1:	 FULL.
prod 1:	 FULL.
prod 1:	 FULL.
prod 1:	 9.
prod 1:	 FULL.
prod 1:	 10.
prod 1:	 FULL.
prod 1:	 17.
prod 1:	 FULL.
prod 1:	 19.
prod 1:	 20.
prod 1:	 21.
prod 1:	 22.
prod 1:	 23.
prod 1:	 FULL.
prod 1:	 24.
prod 1:	 25.
prod 1:	 FULL.
prod 1:	 26.
prod 1:	 27.
prod 1:	 28.
prod 1:	 29.
prod 1:	exited
    
prod 2:	exited
    
prod 3:	 FULL.
prod 3:	 FULL.
prod 3:	 6.
prod 3:	 FULL.
prod 3:	 FULL.
prod 3:	 FULL.
prod 3:	 FULL.
prod 3:	 16.
prod 3:	 FULL.
prod 3:	 18.
prod 3:	exited
    
prod 4:	 FULL.
prod 4:	 FULL.
prod 4:	 FULL.
prod 4:	 7.
prod 4:	 FULL.
prod 4:	 FULL.
prod 4:	 11.
prod 4:	 12.
prod 4:	 13.
prod 4:	 14.
prod 4:	 15.
prod 4:	 FULL.
prod 4:	 FULL.
prod 4:	 FULL.
prod 4:	exited
      

The primary expecation of this lab is to avoid deadlocks for arbitrary number of producers and consumers. You are not required to print out the FULL/EMPTY strings. However, with the semaphore solution, if you do want to print the FULL/EMPTY status of the buffer, you should use the sem_getvalue function to get the status of the semaphores and decide whether the buffer is full or empty accordingly.

There are several other patterns of behavior that you can observe in the output from various runs of the program. The output also varies with the parameters used for the do_work() routine, which simulates work done by each thread outside the critical section. The code as provided uses (5,50) for the consumer which says to execute the CPU bound inner loop 5 times and then block for 50 milliseconds. The producer sets both of these to zero, which obviously makes it more likely that the queue will become full. Try different settings. Even modest amounts of production overhead make it much less likely for the queue to fill.

Testing
To test your code, we will use the `all-tests` target in the Makefile. This will run each test and generate a 'narrativeX.raw' and 'narrativeX.sorted' for each test (X = the test #). There are 4 tests total. The tests run your compiled `producer_consumer` executable as follows:
  1. ./producer_consumer 1 1 # 1 producer, 1 consumer
  2. ./producer_consumer 1 5 # 1 producer, 5 consumers
  3. ./producer_consumer 5 1 # 5 producers, 1 consumer
  4. ./producer_consumer 5 5 # 5 producers, 5 consumers
We will check to see that the output of each test demonstrates proper concurrency control between producer and consumer threads. We will also check your source code to ensure proper implementation.
Submission
You also need to tar up your lab for submission. For this step, you should use the 'tar' target included in the lab's Makefile. Change the 'FIRST_NAME', 'LAST_NAME', and 'KUID' variables in the Makefile to your student ID and type:
    make tar
The total grade for this lab will be awarded based on the submission of your code and your quiz answers and demo.
  1. 75% (Lab submission) - Your submission on the canvas for this lab
  2. 10% (Quiz) - A short quiz about IPC
  3. 15% (Demo) - A quick demo of your submission will be taken on the following week during the lab hours
Please ensure that you have tested your final code on the EECS cycle servers.

 

< Back to the Lab Home Page