Instructions For Exercise
This lesson demonstrates how you can run files provided in the exercise.
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 = ∅ // 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: -l
specifies how
many times each producer should loop (and thus how many data items each
producer produces), -m
controls the size of the shared buffer (greater than or
equal to one), and -p
and -c
set how many producers and consumers there are,
respectively.
What is more interesting are the two sleep strings, one for producers, and one for consumers. These flags allow you to make each thread sleep at certain points in execution and thus switch to other threads; doing so allows you to play with each solution and perhaps pinpoint specific problems or study other facets of the producer/consumer problem.
The string is specified as follows. If there are three producers, for example, the string should specify sleep times for each producer separately, with a colon separator. The sleep string for these three producers would look something like this:
sleep_string_for_p0:sleep_string_for_p1:sleep_string_for_p2
Each sleep string, in turn, is a comma-separated list, which is used to
decide how much to sleep at each sleep point in the code. To understand this
per-producer or per-consumer sleep string better, let’s look at the code in
main-two-cvs-while.c
, specifically at the producer code. In this code
snippet, a producer thread loops for a while, putting elements into the shared
buffer via calls to do\_fill()
. Around this filling routine are some
waiting and signaling code to ensure the buffer is not full when the producer
tries to fill it. See the chapter for more details.
void *producer(void *arg) {
int id = (int) arg;
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(i); p4;
Cond_signal(&fill); p5;
Mutex_unlock(&m); p6;
}
return NULL;
}
As you can see from the code, there are a number of points labeled p0
, p1
,
etc. These points are where the code can be put to sleep. The consumer code
(not shown here) has similar wait points (c0
, etc.).
Specifying a sleep string for a producer is easy: just identify how long the
producer should sleep at each point p0
, p1
, …, p6
. For example, the string
1,0,0,0,0,0,0
specifies that the producer should sleep for 1 second at marker
p0
(before grabbing the lock), and then not at all each time through the loop.
Now let’s show the output of running one of these programs. To begin, let’s
assume that the user runs with just one producer and one consumer. Let’s not
add any sleeping at all (this is the default behavior). The buffer
size, in this example, is set to 2 (-m 2
).
First, let’s build the code. Enter make
in the coding widget given in the start of this lesson.
prompt> make
gcc -o main-two-cvs-while main-two-cvs-while.c -Wall -pthread
gcc -o main-two-cvs-if main-two-cvs-if.c -Wall -pthread
gcc -o main-one-cv-while main-one-cv-while.c -Wall -pthread
gcc -o main-two-cvs-while-extra-unlock main-two-cvs-while-extra-unlock.c
-Wall -pthread
Now we can run something:
prompt> ./main-two-cvs-while -l 3 -m 2 -p 1 -c 1 -v
In this case, if you trace the code (with the verbose flag, -v
), you will get
the following output (or something like it) on the screen:
NF P0 C0
0 [*--- --- ] p0
0 [*--- --- ] c0
0 [*--- --- ] p1
1 [u 0 f--- ] p4
1 [u 0 f--- ] p5
1 [u 0 f--- ] p6
1 [u 0 f--- ] p0
1 [u 0 f--- ] c1
0 [ --- *--- ] c4
0 [ --- *--- ] c5
0 [ --- *--- ] c6
0 [ --- *--- ] c0
0 [ --- *--- ] p1
1 [f--- u 1 ] p4
1 [f--- u 1 ] p5
1 [f--- u 1 ] p6
1 [f--- u 1 ] p0
1 [f--- u 1 ] c1
0 [*--- --- ] c4
0 [*--- --- ] c5
0 [*--- --- ] c6
0 [*--- --- ] c0
0 [*--- --- ] p1
1 [u 2 f--- ] p4
1 [u 2 f--- ] p5
1 [u 2 f--- ] p6
1 [u 2 f--- ] c1
0 [ --- *--- ] c4
0 [ --- *--- ] c5
0 [ --- *--- ] c6
1 [f--- uEOS ] [main: added end-of-stream marker]
1 [f--- uEOS ] c0
1 [f--- uEOS ] c1
0 [*--- --- ] c4
0 [*--- --- ] c5
0 [*--- --- ] c6
Consumer consumption:
C0 -> 3
Before describing what is happening in this simple example, let’s understand
the depiction of the shared buffer in the output, as is shown on the left. At
first, it is empty (an empty slot indicated by ---
, and the two empty slots
shown as [*--- --- ]
); the output also shows the number of entries in the
buffer (num\_full
), which starts at 0. Then, after P0 puts a value in it, its
state changes to [u 0 f---
] (and num\_full
changes to 1). You might also
notice a few additional markers here; the u marker shows where the use\_ptr
is
(this is where the next consumer to consume a value will get something from);
similarly, the f marker shows where the fill\_ptr
is (this is where
the next producer will produce a value). When you see the * marker, it
just means the use\_ptr
and fill\_ptr
are pointing to the same slot.
Along the right, you can see the trace of which step each producer and consumer
is about to execute. For example, the producer grabs the lock (p1
), and then,
because the buffer has an empty slot, produces a value into it (p4
). It then
continues until the point where it releases the lock (p6
) and then tries
to reacquire it. In this example, the consumer acquires the lock instead and
consumes the value (c1
, c4
). Study the trace further to understand
how the producer/consumer solution works with a single producer and consumer.
Now let’s add some pauses to change the behavior of the trace. In this case, let’s say we want to make the producer sleep so that the consumer can run first. We can accomplish this as follows:
prompt> ./main-two-cvs-while -l 1 -m 2 -p 1 -c 1 -P 1,0,0,0,0,0,0 -C 0 -v
The results:
NF P0 C0
0 [*--- --- ] p0
0 [*--- --- ] c0
0 [*--- --- ] c1
0 [*--- --- ] c2
0 [*--- --- ] p1
1 [u 0 f--- ] p4
1 [u 0 f--- ] p5
1 [u 0 f--- ] p6
1 [u 0 f--- ] p0
1 [u 0 f--- ] c3
0 [ --- *--- ] c4
0 [ --- *--- ] c5
0 [ --- *--- ] c6
0 [ --- *--- ] c0
0 [ --- *--- ] c1
0 [ --- *--- ] c2
...
As you can see, the producer hits the p0 marker in the code and then grabs the first value from its sleep specification, which in this case is 1, and thus each sleeps for 1 second before even trying to grab the lock. Thus, the consumer gets to run, grabs the lock, but finds the queue empty, and thus sleeps (releasing the lock). The producer then runs (eventually), and all proceeds as you might expect.
Do note: a sleep specification must be given for each producer and
consumer. Thus, if you create two producers and three consumers (with -p 2 -c 3
, you must specify sleep strings for each (e.g., -P 0:1
or -C 0,1,2:0:3,3,3,1,1,1
). Sleep strings can be shorter than the number of sleep
points in the code; the remaining sleep slots are initialized to be zero.
Get hands-on with 1400+ tech skills courses.