.Net provides a number of ways to manage threading, and the Internet has no shortage of articles explaining how to use them. Unfortunately, I still found myself having to look up examples every time I was going to use threads of any sort. The information wasn't sticking, because I didn't fundamentally understand what was going on. The goal of this is to present the technology in a more intuitive way.
As mentioned, there are a number of ways to handle threading in .net. I'll hopefully get to all of them in time, but I'm starting with delegates for two reasons:
- They're used heavily throughout the framework
- They're one of the most confusing concepts.
Before I get started, make sure you're comfortable with what a thread is. A brief google search led me to a decent article on this here. The following two terms are also critical to understanding the rest of the post.
- ThreadPool - Threads are fairly expensive to create and destroy. You also need to be careful that you don't spawn too many at a time, because CPU time is consumed when switching between one thread and another. As a result, the CLR manages a pool of threads for you. This allows you to queue as much work as you want, trusting the CLR to manage things efficiently for you.
- Delegates - It's easy to be mystified by how delegates work, because so much is done for you by the compiler behind the scenes. To briefly summarize:
- A delegate is often described as a function pointer, a variable that represents a function. This is a true, but not entirely incomplete description.
- Delegates are classes that inherit from System.MulticastDelegate.
It may help to think of your delegate declaration as being very similar to defining a variable of a generic class. (eg: Delegate<RETURNTYPE, ARG1, ARG2> myDelegate). The syntactic help you get is really smoke and mirrors provided by the compiler.
private delegate void MyDelegateType(object message);
static void Main(string[] args)
{
MyDelegateType myDelegateVariable =
new MyDelegateType(PrintToConsole.Print);
// The following two lines do the exact same thing
myDelegateVariable.Invoke("Hello");
myDelegateVariable("Hello");
}
Using Delegates for Asynchronous Execution
The BeginInvoke / EndInvoke pattern is repeated throughout the framework, so it's a good idea to at least understand this one, even if it doesn't wind up being your preferred strategy.
Using BeginInvoke and EndInvoke follows a similar process to what happens when you go out to eat. Lets introduce our characters:
- Customer - Our hapless calling method. Likes steak.
- Waitress - Our asynchronous delegate.
- Cook - Our easily disgruntled, time consuming worker method.
Now, briefly consider what would happen if this process was a synchronous one. Our customer comes in and orders food, but then, rather than talking on their cell at an obscene volume, or making decidedly half-hearted attempts to control their kids, they just freeze. They don't move, they don't speak. It's a peaceful place, and it makes the waitress' job easier, because all she has to do is bring the order to the cook. There will be no need for breadsticks, or drink refills. The cook takes the order, and begins working on the food. It's done when it's done. There's no hurry, and no complaint. The cook makes the food, and puts it up when he's good and ready. Our waitress immediately snatches it up, and brings it to the customer, who all of the sudden wakes up and begins eating, thinking how fast the service here is.
This is how most of our code generally operates. Generally, it makes sense to work in this way. In this case it doesn't. Below is code that works in a manner more consistent with your past restaurant experiences. Take a look at the Console statements, as I'm trying to equate the code to the restaurant scenario.
public class Cook
{
static internal Dinner Make(Order order)
{
Console.WriteLine("\tCook: Start Cooking... " + order.foodOrder);
Thread.Sleep(5000);
if (order.customer.IsAnnoying)
Console.WriteLine("\tCook: Applying special sauce...");
return new Dinner();
}
}
class RestaurantProgram
{
private delegate Dinner WaitressHandler(Order order);
static void Main(string[] args)
{
Console.WriteLine("Customer enters the restaurant");
Customer customer = new Customer();
Console.WriteLine("A waitress comes over to take the customer's order.");
WaitressHandler customersWaitress = new WaitressHandler(Cook.Make);
Console.WriteLine("Customer: I'd like the steak and potatoes please.");
Order order = new Order("Steak and Potatoes", customer);
Console.WriteLine("Waitress takes your order, and brings it to the cook.");
IAsyncResult result = customersWaitress.BeginInvoke(order, null, null);
int numMinutesWaited = 0; // minutes = seconds...
while(!result.IsCompleted)
{
if (numMinutesWaited == 0)
Console.WriteLine("Customer commences people-watching. Drips salsa all over the table.");
else if(numMinutesWaited == 1)
Console.WriteLine("Customer is out of chips, running short of patience.");
else if (numMinutesWaited == 2)
{
Console.WriteLine("Customer complains about the wait... such a bad idea.");
customer.IsAnnoying = true;
}
numMinutesWaited += 1;
Thread.Sleep(1000);
}
Console.WriteLine("Food is prepared, and the waitress brings it to the customer");
Dinner dinner = customersWaitress.EndInvoke(result);
Console.ReadLine();
}
}
And the output:
Ok, let's take a look at what our waitress is doing. First, she takes the order, using BeginInvoke. Using this method places the customer's order in a queue (ThreadPool) for the cook. You can think of this as the cook's grill. He can only cook so many things at a time.
What is that IAsyncResult object? This object helps us keep track of the cook's progress. In this code, we check the IsCompleted property to see if the food is ready. As you can see, every second of delay increases our customer's agitation. Eventually, IsComplete will be true, meaning that dinner is ready. When this happens, we fall out of the while loop, and the waitress bring our customer's food over with the EndInvoke method. The result object we pass in to EndInvoke is akin to the tickets you see posted over a completed meal on the Ready counter. It's what tells our waittress whose dinner has just been finished, which allows her to determine which customer it belongs to.
Hopefully, this analogy helps explain what delegate are for, and provides some insight into how they work. I'll go over alternative ways to handle your asynchronous code with delegates in Part 2.