Instructions For Exercise

This lesson demonstrates how you can run files provided in the exercise.

We'll cover the following...

This exercise lets you explore some real code that uses locks and condition variables to implement various forms of the producer/consumer queue discussed in the chapter. You’ll look at the real code, run it in various configurations, and use it to learn about what works and what doesn’t, as well as other intricacies.

The different versions of the code correspond to different ways to “solve” the producer/consumer problem. Most are incorrect; one is correct. Read the chapter to learn more about what the producer/consumer problem is, and what the code generally does.

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <sys/time.h>
#include <string.h>

#include "mythreads.h"

#include "pc-header.h"

// used in producer/consumer signaling protocol
pthread_cond_t empty  = PTHREAD_COND_INITIALIZER;
pthread_cond_t fill   = PTHREAD_COND_INITIALIZER;
pthread_mutex_t m     = PTHREAD_MUTEX_INITIALIZER;

#include "main-header.h"

void do_fill(int value) {
    // ensure empty before usage
    ensure(buffer[fill_ptr] == EMPTY, "error: tried to fill a non-empty buffer");
    buffer[fill_ptr] = value;
    fill_ptr = (fill_ptr + 1) % max;
    num_full++;
}

int do_get() {
    int tmp = buffer[use_ptr];
    ensure(tmp != EMPTY, "error: tried to get an empty buffer");
    buffer[use_ptr] = EMPTY; 
    use_ptr = (use_ptr + 1) % max;
    num_full--;
    return tmp;
}

void *producer(void *arg) {
    int id = (int) arg;
    // make sure each producer produces unique values
    int base = id * loops; 
    int i;
    for (i = 0; i < loops; i++) {   p0;
	Mutex_lock(&m);             p1;
	while (num_full == max) {   p2;
	    Cond_wait(&empty, &m);  p3;
	}
	do_fill(base + i);          p4;
	Cond_signal(&fill);         p5;
	Mutex_unlock(&m);           p6;
    }
    return NULL;
}
                                                                               
void *consumer(void *arg) {
    int id = (int) arg;
    int tmp = 0;
    int consumed_count = 0;
    while (tmp != END_OF_STREAM) { c0;
	Mutex_lock(&m);            c1;
	while (num_full == 0) {    c2;
	    Cond_wait(&fill, &m);  c3;
        }
	tmp = do_get();            c4;
	Cond_signal(&empty);       c5;
	Mutex_unlock(&m);          c6;
	consumed_count++;
    }

    // return consumer_count-1 because END_OF_STREAM does not count
    return (void *) (long long) (consumed_count - 1);
}

// must set these appropriately to use "main-common.c"
pthread_cond_t *fill_cv = &fill;
pthread_cond_t *empty_cv = &empty;

// all codes use this common base to start producers/consumers
// and all the other related stuff
#include "main-common.c"



Pay attention to these four files:

  • main-one-cv-while.c: The producer/consumer problem solved with a single condition variable.
  • main-two-cvs-if.c: Same but with two condition variables and using an if to check whether to sleep.
  • main-two-cvs-while.c: Same but with two condition variables and while to check whether to sleep. This is the correct version.
  • main-two-cvs-while-extra-unlock.c: Same but releasing the lock and then reacquiring it around the fill and get routines.

It’s also useful to look at pc-header.h which contains common code for all of these different main programs, and the Makefile so as to build the code properly.

Each program takes the following flags:

  -l <number of items each producer produces>
  -m <size of the shared producer/consumer buffer>
  -p <number of producers>
  -c <number of consumers>
  -P <sleep string: how the producer should sleep at various points>
  -C <sleep string: how the consumer should sleep at various points>
  -v [verbose flag: trace what is happening and print it]
  -t [timing flag: time entire execution and print total time]

The first four arguments are relatively self-explanatory: ...