Skip to content

Latest commit

 

History

History
 
 

30.第三十章-条件变量

This homework 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.

The first step is to download the code and type make to build all the
variants. You should see four:

- 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 producer should sleep at various points>
  -C <sleep string: how 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 an 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  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:

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 [ --- *--- ]    c6runs
  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.