Lab 10 - Threads

In this lab, you'll explore how to further utilize threads and also work with delegates and how to generate and handle your own events.

The purpose of this program we're building is to have an array of doubles and have the worker threads each process a piece of the array.  For example, if we have 4 worker threads, then each could process 1/4th of the array.  Then, all the values computed could be combined to generate a final result.  This process is quite common in parallel processing.

Here are the steps:

  1. Build a Worker class that will use threading to perform some calculations on values
  2. Define an event that handles when the calculations are complete
  3. In the main method, create a handler for the event
  4. Put it all together and create a program that uses the Worker class to parallel process a collection of numbers

Step 1 - Build the Worker class

One problem we have when we start a thread is that you can't pass any parameters to the method that the thread will start in.  The ThreadStart class constructor doesn't take in any parameters, so if we want to pass parameters from which our thread works, we need to do something else.

Consider this process:

  1. Define a class and pass it parameters to its constructor
  2. Set private attributes of the class based upon the parameters passed into the class' constructor
  3. Have this class then generate the thread (invoking a private method within the class for ThreadStart)
  4. The private method the thread runs can access the private attributes of the class

Consequently, by making the new class responsible for creating the thread, we now have a way of passing parameters to the thread's worker method.

In this step of the lab, create a new class called Worker that has three private attributes: a double[] called values, an int called start_index, and an int called end_index.  Then create a constructor that takes in values for each attribute and sets the attributes to the values passes as parameters to the constructor.  Then create a method DoWork that creates a thread that will execute a private method calculate; start the thread from within this DoWork method.

In the calculate method, loop across the array from the start_index to the end_index (but exclude the end index value) and generate the product (multiplication of) all the values in that portion of the array.  Store the value computed into a local variable local_product.

Click here to see a solution for this step of the lab.


Step 2 - Define the event

In the words of Monty Python, "and now for something completely different..."

We're going to introduce two new topics now that are important to making your programs flexible and extensible.  You already know how to respond to events that are generated in GUI programs (the buttonClick event, for example, is bound to a method you write, and that code is executed when the event is raised), but how would you write your events and respond to them?  In this section of the lab, we'll do just that!

First, you can define a "template" for what a method must look like if you want to bind your code to it by using a delegate.  A delegate is akin to a method pointer or template.  The definition looks like this:

delegate <RETURN_TYPE> <NAME>(<PARAMETERS>);

So if we wanted to define a method template that returns nothing and takes in a double, then we could define:

delegate void ResultHandler(double value);

Now that this is defined, we can create an event within our Worker class, and then raise that event explicitly whenever we want; it would then be up to someone else to respond to that event if they were bound to it.

We can define events like this:

event <DELEGATE> <EVENT_NAME>;

And since we've already defined our delegate, then we can define the event we want as:

event ResultHandler resultFinished;

Place these into the Worker class (make then public within the class), and then raise the resultFinished event at the end of the calculate method (passing in the local_product as the parameter to the event).

Click here to see a solution to this step of the lab.


Step 3 - Create a handler for the event

Now that we've completed the Worker class, we can start making use of it.  Back in program.cs, we'd like to define a handler for the resultFinished event such that when the resultFinished event is raised, a method is invoked within program.cs.

It's the job of the main program to collect all of the locally-generated products from each Worker and combine them into one overall product.  To do this, the main program needs a total_product (double) attribute, and it also needs a completed_count (int) attribute.  Add these into your program class.  Next, in Main, initialize the total_product to 1.0 and completed_count to 0.  Also, create a local (within Main) double[] like this:

double[] nums = {1.3, 7.4, 2.9, 5.5, 4.2, 10.3,
                 3.3, 2.1, 7.6, 4.3, 8.7, 1.1 };

This will initialize the nums array to the values specified; of course, you can use whatever values you'd like.

Now it's time to respond to the resultFinished method.  Define a new method within the program class that is called UpdateTotalProduct and takes in a double via parameter and returns void.

The work of this method is to take the parameter and update the total_product by this amount (using *=).  It should also increase completed_count by 1 and if completed_count = 3, then print the result (notice we only want to display the result, i.e., total_product, if all the workers have completed).

Click here to see a solution to this step of the lab.


Step 4 - Finish the main program

Finally - we're ready to create objects of the Worker class and then have them perform the calculations.  Notice that when we create a Worker object, we pass three parameters to its constructor.  Additionally, we'll invoke the DoWork method.  But before we invoke the DoWork method, we need to bind the UpdateTotalProduct method to the resultFinished event for the Worker objects.  We can do this simply (as the Visual Studio IDE does for us automatically in GUI applications) by adding the handler to the resultFinished attribute of the Worker object (using +=).

Create a loop that generates four Worker objects, and invoke the DoWork for each object.  Also, bind the UpdateTotalProduct method to the resultFinished event for each worker (do this before calling DoWork or you'll get a runtime exception).

Click here to see a solution to this step of the lab.


Wrapping it all up

Click here to see the finished ZIPed solution.