This lab was developed by Prof. L. Felipe Perrone. Permission to reuse this material in parts or in its entirety is granted provided that this credits note is not removed. Additional students files associated with this lab, as well as any existing solutions can be provided upon request by e-mail to: perrone[at]bucknell[dot]edu
Assume that you have a circular-list (a.k.a. circular-queue) with n buffers, each capable of holding a single value of data type double. If this list is to be shared by multiple threads, you need to be careful with how you implement functionality to remove and to return a buffer to the data structure. By virtue of their scheduling, threads might interrupt each other in the middle of one of these operations leaving the data structure in an inconsistent state.
Your textbook, Operating Systems Concepts 10th Ed., by Silberschatz, Galvin, and Gagne, discusses the bounded-buffer problem in the context of the classical synchronization problem of producers and consumers (Section 7.1). The solution to the problem is presented in structures that delineate the code for the two types of process as shows below.
do {
...
// produce an item i
...
wait(empty);
wait(mutex);
// add item i to buffer
signal(mutex);
signal(full);
} while (TRUE);
do {
...
// consume an item i
...
wait(full);
wait(mutex);
// remove item i from buffer
signal(mutex);
signal(empty);
} while (TRUE);
Looking at the structure of code given above, you will realize that having the producers and the consumer processes deal directly with synchronization is not ideal. Primarily, there is an issue of applying the concept of abstraction: the code would be substantially easier to manage if you had an ADT for the circular-list. The work you did for the pre-lab implements the following application programming interface (API):
/**
* Create a circular list with a pre-defined buffer size.
* @param l pointer to a circular list ADT
* @param size number of items to allocate in circular list
* @return 0 if successful, -1 if any error condition is found
*/
int circular_list_create(struct circular_list *l, int size);
/**
* Insert item into the circular list.
* @param l pointer to a circular list ADT
* @param i item to copy into a position of the circular list
* @return 0 if successful, -1 if any error condition is found
*/
int circular_list_insert(struct circular_list *l, item i);
/**
* Remove item from the circular list.
* @param l pointer to a circular list ADT
* @param i pointer to an item onto which the removed item is copied
* @return 0 if successful, -1 if any error condition is found
*/
int circular_list_remove(struct circular_list *l, item *i);
Now that you have a single-thread implementation for these functions, you can work to build synchronization into them. Once you have that, the producer and consumer can safely call circular_list_insert and circular_list_remove in a multi-threaded context because the synchronized ADT takes proper care of things. By now, you must have completed the implementation of the ADT and of a program that exercises that implementation (which we called adt-test.c in the pre-lab). If you haven’t fully tested and debugged your circular list, do not move on to the remainder of this lab.
Once your ADT works well for a single-threaded program, spend some time identifying which synchronization mechanisms you can hide behind the API of your circular list. Your goal is to modify the ADT so that it works safely in a multi-threaded program. Your textbook gives you a nearly complete solution to implement (9th edition SGG, Chapter 5, Project 3, which is reproduced at the bottom of this lab).
prodcons [num_prod] [num_cons] [sleep_time]
where:
It should go with saying that you should experiment with your code extensively to convince yourself that there are no obvious bugs, that your implementation works as expected.
IMPORTANT: In order to shake this program around to expose bugs, you will need to ensure that every producer thread has a different seed for random number generation. Think about how you can pass into each thread an integer value and use that value to seed the thread’s random number generator.
The following notes may help you debug your program and verify if your program works properly.
When you are done with this, you need to:
Citations are to content in SGG 5, some of which is translated to 10th in brackets [].
Project 3 —Producer – Consumer Problem
In Section 5.7.1 [7.1.1], we presented a semaphore-based solution to the producer– consumer problem using a bounded buffer. In this project, you will design a programming solution to the bounded-buffer problem using the producer and consumer processes shown in Figures 5.9 [7.1] and 5.10 [7.2]. The solution presented in Section 5.7.1 [7.1.1] uses three semaphores: empty and full, which count the number of empty and full slots in the buffer, and mutex , which is a binary (or mutual-exclusion) semaphore that protects the actual insertion or removal of items in the buffer. For this project, you will use standard counting semaphores for empty and full and a mutex lock, rather than a binary semaphore, to represent mutex . The producer and consumer—running as separate threads—will move items to and from a buffer that is synchronized with the empty , full , and mutex structures. You can solve this problem using either Pthreads or the Windows API.
#include "buffer.h" /* the buffer */ buffer_item buffer[BUFFER_SIZE]; int insert_item(buffer_item item) { /* insert item into buffer return 0 if successful, otherwise return -1 indicating an error condition */ } int remove_item(buffer_item *item) { /* remove an object from placing it in item return 0 if successful, return -1 otherwise indicating an error condition */ }
Figure 5.24 Outline of buffer operations.
The Buffer
Internally, the buffer will consist of a fixed-size array of type buffer item
(which will be defined using a typedef ). The array of buffer item objects
will be manipulated as a circular queue. The definition of buffer item , along
with the size of the buffer, can be stored in a header file such as the following:
/* buffer.h */ typedef int buffer_item; #define BUFFER_SIZE 5
The buffer will be manipulated with two functions, insert item() and
remove item() , which are called by the producer and consumer threads,
respectively. A skeleton outlining these functions appears in Figure 5.24.
The insert item() and remove item() functions will synchronize the
producer and consumer using the algorithms outlined in Figures 5.9 [7.1] and 5.10 [7.2]. The buffer will also require an initialization function that initializes the mutual-exclusion object mutex along with the empty and full semaphores.
The main() function will initialize the buffer and create the separate
producer and consumer threads. Once it has created the producer and
consumer threads, the main() function will sleep for a period of time and,
upon awakening, will terminate the application. The main() function will be
passed three parameters on the command line:
1. How long to sleep before terminating
2. The number of producer threads
3. The number of consumer threads
#include "buffer.h" int main(int argc, char *argv[]) { 1. Get command line arguments argv[1],argv[2],argv[3] */ 2. Initialize buffer */ 3. Create producer thread(s) */ 4. Create consumer thread(s) */ 5. Sleep */ 6. Exit */ }
Figure 5.25 Outline of skeleton program.
A skeleton for this function appears in Figure 5.25.
The Producer and Consumer Threads
The producer thread will alternate between sleeping for a random period of
time and inserting a random integer into the buffer. Random numbers will
be produced using the rand() function, which produces random integers
between 0 and RAND MAX . The consumer will also sleep for a random period of time and, upon awakening, will attempt to remove an item from the buffer. An outline of the producer and consumer threads appears in Figure 5.26.
As noted earlier, you can solve this problem using either Pthreads or the
Windows API . In the following sections, we supply more information on each of these choices.
Pthreads Thread Creation and Synchronization
Creating threads using the Pthreads API is discussed in Section 4.4.1. Coverage of mutex locks and semaphores using Pthreads is provided in Section 5.9.4 [7.3.2]. Refer to those sections for specific instructions on Pthreads thread creation and synchronization.