In this exercise you are going to program a classical "producer-consumer" application.
One thread (the producer) produces a items (in this case simple int's). Another class (the consumer) consumes items. For communication purposes both threads have access to a bounded buffer which is basically a queue (add elements in in one end, and take element out from the other end).
Picture from http://crunchify.com/java-producer-consumer-example-handle-concurrent-read-write/
As a specification you the some documentation of the Producer-Consumer framework. Note that this documents the final state of the framework, not the first stages.
Program a class called BoundedBuffer.
The class BoundedBuffer must be thread safe.
Refer to the specification for more details. Do not take the AbstractBuffer or the IBuffer into account yet. It will be used in a later stage in this exercise.
Hint: Use a Queue to keep the elements in the bounded buffer. You may have to read the documentation on the Queue class to learn how to use it.
Unit test the BoundedBuffer class: Do not Add more elements into the buffer than the capacity allows (if you do so the test will hang).
Program a class Producer which produces elements and adds the elements into a BoundedBuffer.
Refer to the specification for more details.
The Producers run() method should basically have a for loop where an element is added into the buffer at each turn in the loop.
Every time the Producer has added a new item into the BoundedBuffer the producer should write to the screen using Console.WriteLine(...)
Program a class Consumer which consumes (takes) elements from a BoundedBuffer.
Refer to the specification for more details. In this first version of the Consumer the constructor should have two parameters, like the Producer constructor. Later in this exercise we will get rid of on of the parameters.
The Consumers run() method basically have a for loop where an element is taken from the buffer at each turn in the loop.
Every time the Consumer has taken an element it should do Console.WriteLine(...)
Program a class with a main(...) method.
The main(...) method should make 1 BoundedBuffer, 1 Producer, and 1 Consumer - and start the Producer and Consumer objects as Threads.
Extra: What happens if 2 or more Consumers (or Producers) share a single buffer?
In the current version the Consumer must know how many elements to consumer: The Consumer has a parameter "howMany" in its constructor.
Now it is time to get rid of this "howMany" parameter in the Consumers constructor!
The new idea is that the Producer adds a special element called LastElement into the buffer as the very last element. When the Producer sees LastElement the Producer knows that it can stop.
Refactor the classes to implement this new idea:
public static readonly int LastElement = -1
) and add it into the buffer after adding all the ordinary elementsMake a main with 1 producer and 2(!) consumers sharing the same buffer. When you run this application, you will probably see that it does not stop. The reason is that only one of the consumers gets the special LastElement - the other consumer will be waiting forever (or until you kill the application).
We will solve this problem later ...
The Middelman act as a combined producer and consumer. The MiddleMan takes from one buffer and adds into another buffer. The Middleman constructor must have 2 parameters: 1 ingoing buffer + 1 outgoing buffer.
A duplicates has 1 input buffer and 2 output buffers: Copying everything from the input buffer to both output buffers.
Extra: Can be generalized to have a List or Array of output buffers.
Program a class UnboundedBuffer. Very much like BoundedBuffer, except that it is unbounded, i.e. Add() will never have to wait.
To make BoundedBuffer and UnboundedBuffer work seamlessly with the Producer and Consumer you must "extract" an interface Buffer from BoundedBuffer and UnboundedBuffer.
Boundedbuffer and UnboundedBuffer probably has some duplicate code (that is code that is repeated in both classes).
Program an abstract class AbstractBuffer to implement this duplicate code in a single place.
Don't forget to change an re-run main.
Some worker classes, like MiddleMan and Duplicator needs access to rather different buffers. For example MiddleMan needs 1 buffer to read from and 1 buffer to write to.
Idea: Make two super-interfaces of Buffer:
The IBuffer interface will then be a simple combination of the ITakeBuffer and the IAddBuffer
public interface IBuffer : IAddBuffer, ITakeBuffer { /* otherwise empty */ }
I guess your main haven't used thread pools until now.
Try to use a thread pool.
Earlier in the exercise you encountered a problem if there is one producer, one buffer, and two consumers.
Now it is time to solve the problem.
A way to solve this problem is to change the behavior of the buffer slightly: When the buffer encounters the special LastElement, it must "remember" that this has happened - and when other threads (consumers) later calls the take() method, the LastElement must be returned - any number of times.
Trying to Add a new element AFTER the LastElement is illegal and should throw InvalidOperationException.
The C# API has a class that works almost like BoundedBuffer (and UnboundedBuffer for that matter). This class is called BlockingCollection.
Read about it and try to refactor you Producer-Consumer framework from using your own buffers to use BlockingCollection.
Make the Producer-Consumer framework more flexible: It should be able to handle any kind of elements using generics.
Simple concept:
You get a lot of windows, that can be arranged on the screen, manually.
More advanced GUI's are possible as well. With inspiration from the GUI builder in Visual Studio or Microsoft Blend you can program something similar