I am studying mutual exclusion in college, and we just covered the producer/consumer problem. The class does not involve writing code, but I decided to implement a bounded buffer version of this problem. I have never written a multi-threaded program before, nor have I written a program with mutual exclusion before, so I decided to request a review here.
I implemented three variations, a busy-waiting variation, a Semaphore variation, and a Monitor variation.  All of these reside in a class named Program, which is needed for the threading.  The Monitor variation looks as if there should be a simpler solution with fewer variables. Is this so?
This is the part of the code that never changes:
const int buffSize = 10;
static char[] buffer = new char[buffSize];
static int valuesToProduce = 95;
static void Main(string[] args)
{
    Thread p = new Thread(new ThreadStart(Program.produce));
    Thread c = new Thread(new ThreadStart(Program.consume));
    p.Start();
    c.Start();
}
This is the busy-waiting producer/consumer and their related global variable:
static int avail = 0;
static void produce()
{
    for(int i=0; i<valuesToProduce; i++)
    {
        while (avail == buffSize) { };
        buffer[i % buffSize] = (char)(32 + i % 95);
        Console.WriteLine("Produced: {0}", buffer[i % buffSize]);
        avail++;
    }
}
static void consume()
{
    for (int i = 0; i < valuesToProduce; i++)
    {
        while (avail < 1) { };
        char c = buffer[i % buffSize];
        Console.WriteLine("Consumed: {0}", buffer[i % buffSize]);
        avail--;
    }
}
This is the Semaphore implementation:
private static Semaphore isFull = new Semaphore(buffSize, buffSize);
private static Semaphore isEmpty = new Semaphore(0, buffSize);
static void produce()
{
    for (int i = 0; i < valuesToProduce; i++)
    {
        isFull.WaitOne();
        buffer[i % buffSize] = (char)(32 + i % 95);
        Console.WriteLine("Produced: {0}", buffer[i % buffSize]);
        isEmpty.Release(1);
    }
}
static void consume()
{
    for (int i = 0; i < valuesToProduce; i++)
    {
        isEmpty.WaitOne();
        char c = buffer[i % buffSize];
        Console.WriteLine("Consumed: {0}", c);
        isFull.Release(1);
    }
}
And this is the Monitor implementation:
static int avail = 0;
private static object _buffer = new object();
private static object isFull = new object();
private static object isEmpty = new object();
static void produce()
{
    for (int i = 0; i < valuesToProduce; i++)
    {
        while (avail == buffSize)
        {
            Monitor.Enter(isFull);
            Monitor.Wait(isFull);
            Monitor.Exit(isFull);
        }
        Monitor.Enter(_buffer);
        buffer[i % buffSize] = (char)(32 + i % 95);
        avail++;
        Console.WriteLine("Produced: {0}", buffer[i % buffSize]);
        Monitor.Exit(_buffer);
        Monitor.Enter(isEmpty);
        Monitor.Pulse(isEmpty);
        Monitor.Exit(isEmpty);
    }
    avail++;
}
static void consume()
{
    for (int i = 0; i < valuesToProduce; i++)
    {
        while (avail < 1)
        {
            Monitor.Enter(isEmpty);
            Monitor.Wait(isEmpty);
            Monitor.Exit(isEmpty);
        }
        Monitor.Enter(_buffer);
        char c = buffer[i % buffSize];
        avail--;
        Console.WriteLine("Consumed: {0}", buffer[i % buffSize]);
        Monitor.Exit(_buffer);
        Monitor.Enter(isFull);
        Monitor.Pulse(isFull);
        Monitor.Exit(isFull);
    }
}