Multithreaded programming can be a real pain. In the old days, we have to deal with creating and managing threads. It was a chore. However, the .NET Task Parallel Library (TPL) makes programming asynchronous operations, the usual work of threads, a lot less painful and a lot more fun.
This is a Down and Dirty article. The goal here is to give you the basics you need to be operational in TPL programming without a lot of theoretical overhead. The article is meant to be fast and simple. You’re going get some basic concepts while looking a lot of code. Then, if you feel inspired, you can look to other references to get the details you need to dive more deeply into TPL.
To get full benefit from reading this article, we expect that you can read and program in C#. Also, we assume that you understand the basics of lambda expressions and generics. If you have these basics, you are ready to get down and dirty.
Understanding a Task
Before you go anywhere with TPL, you need to understand the shortcomings of .NET thread programming. The benefit of using multiple threads is that you to do concurrent programming. Methods in a single threaded environment run sequentially. In a multithreaded environment, methods can run simultaneously (see Figure 1).
Figure 1: Methods run sequentially in a single thread; concurrently in a multithreaded environment
Where threading becomes really powerful is on computers having many cores. (Think of a core as a CPU.) Theoretically, when you create multiple threads, the operating system is supposed to assign each thread to a core. In reality, in .NET when you use a Thread object, sometimes the thread runs on a distinct core and sometimes it doesn’t (see Figure 2).
Figure 2: Sometimes, a .NET Thread will not run on its own thread.
TPL makes it so that you can do reliable multithread programming across multiple cores.
All Cores, All the Time
The Task Parallel Library introduced in .NET 4.5 has automagic that allows you to spawn threads that really do get assigned to a distinct core (see Figure 3).
Figure 3: The Task Parallel Library ensures that threads get assigned to cores, when available.
The way that .NET and the Task Parallel Library fixed this thread issue is to create a thread mechanism called a Task. You can think of Task as an asynchronous operation. Not only do you get the execution isolation that comes with threading, you get functionality that makes programming threads a lot easier.
Working with a Task
As mentioned earlier, a Task describes an asynchronous operation. When you start a Task, you’ll pass the Task a lambda expression that indicates the behavior the Task is to execute. That lambda expression can go to a named method or an anonymous method.
There are a few ways to create and run a Task. One way is to use the static method Task.Run(). The method SendDefaultMethod() in the class DownAndDirtyMessenger shown in Listing 1 illustrates how to use Task.Run. (The class, DownAndDirtyMessenger, is the code example that we’ll use throughout this article.)
The method SendDefaultMessage() uses Task.Run() to start a Task that runs the method SimpleSend(), asynchronously. Task.Run() takes as an argument a lambda expression that goes to SimpleSend(). Also, the Task that is created is returned by the method, SendDefaultMessage(). This Task is passed to any code that calls DownAndDirtyMessenger.SendDefaultMessage(). Working with the Task returned by SendDefaultMessage() is addressed later in this article.
namespace reselbob.demos.tpl { public class DownAndDirtyMessenger { private string _defaultMessage = "Default Message"; private void SimpleSend() { Console.WriteLine(_defaultMessage); } public Task SendDefaultMessage() { return Task.Run(() => SimpleSend()); } . . . }
Listing 1: You create a Task with a lambda expression.
Using Anonymous Methods in a Task
Another way start a Task is by using a Task.Factory, as shown in Listing 2.
public Task<string> SendMessage(string message, int secondsToWait = 1) { Task<string> task = Task.Factory.StartNew(() => { //start anonymous method here var msg = message; if (string.IsNullOrEmpty(message)) msg = _defaultMessage; var inTime = DateTime.Now; Thread.Sleep(secondsToWait * 1000); var rtn = string.Format("I am sending this Message:{0} from within the Tasks, Time in: {1}, Time out: {2}", msg, inTime.ToString(_fmt), DateTime.Now.ToString(_fmt)); Console.WriteLine(rtn); return rtn; //return the string as the TResult }); return task; }
Listing 2: You can define the entire behavior of a given Task within the lambda expression argument.
Let take a close look at two parts of Listing 2, the method signature and the execution of an anonymous method that the lambda expression goes to.
First, the method signature.
public Task<string> SendMessage(string message, int secondsToWait = 1){......}
Notice, please, that SendMessage() returns a generic, Task<string>. What’s really going here is that .NET is saying that the method SendMessage() is going to return a Task with a TResult of type, string. What is a TResult? Hang tight; we’ll get to TResult shortly.
Let’s move on the second part, the anonymous method used in the lambda expression. Whereas in Listing 1, we created a Task that had a lambda expression that goes to a named method, SimpleSend(), in Listing 2 the lambda expression goes to an anonymous method. Also, the anonymous method returns a string. This returned string is the TResult that is defined as the return type of SendMessage(), the type, Task<string>.
Let’s take a closer look at TResult.
Working with TResult
Let’s look at a simple method signature:
public int AddNumbers(int x, int y){return x +y;};
It’s pretty straightforward. We have a method, AddNumbers(), that returns a simple type, int.
However, when we deal with a method that returns a Task object, we have to do things a bit differently.
The way you define a Task that returns a result is:
Task<TResult>
WHERE TResult is the type returned by the asynchronous operation.
Let’s look again at the method signature for SendMessage().
public Task<string> SendMessage(string message, int secondsToWait = 1)
Remember, that a Task represents an asynchronous operation. So, the return from SendMessage() is a reference to the Task it created. But, if we remember back to Listing 2, the anonymous method in SendMessage() returned a string. How can this happen, a method having two return types? Well, not really. Remember, the return type for SendMessage() is: Task<string>. It’s generic in which the string is the TResult of the Task.
So, when we declare a return type of Task<string>, what we are saying is “return a Task with a TResult of type, string.” The property, Task.Result, is where the TResult lives. Listing 3 shows how we can access the TResult of a Task, when used in a ContinueWith() method. (ContinueWith() is a method of a Task that gets executed when the Task completes.)
var messenger = new DownAndDirtyMessenger(); var task = messenger.SendMessage(null); task.ContinueWith((t) => { Console.WriteLine("The TResult value is: " + t.Result); });
Listing 3: TPL automagically passes the Task into the ContinueWith() lambda expression as the parameter, t
One of the nice things about TResult is that can create a custom class to be used as your TResult type. Listing 4 shows a class, MessengerResult, that represents a custom class for the TResult that is used by the demonstration class, DownAndDirtyMessenger.
using System; namespace reselbob.demos.tpl { public enum SendStatus { Success, Fail } public class MessengerResult { public string Message { get; set; } public DateTime SendTime { get; set; } public DateTime ReceivedTime { get; set; } public SendStatus SendStatus { get; set; } } }
Listing 4: You can create a custom class for your TResult
Listing 5 shows you how to use a custom class as TResult. The method, SendMessageEx(), declares a return type of Task<MessengerResult>. The class, MessengerResult, is the TResult of the returned Task.
public Task<MessengerResult> SendMessageEx(string message, int secondsToWait = 1) { Task<MessengerResult> task = Task.Factory.StartNew(() => { var msg = message; if (string.IsNullOrEmpty(message)) msg = _defaultMessage; var inTime = DateTime.Now; // put in some time-consuming behavior Thread.Sleep(secondsToWait * 1000); var outTime = DateTime.Now; var rtn = string.Format(msg); return new MessengerResult { Message = msg, ReceivedTime = inTime, SendTime = outTime }; }); return task; }
Listing 5: Using a custom class as a TResult
Let’s take a closer look at Task.ContinueWith() now.
Using Task.ContinueWith()
There are a number of properties and methods that allow you work with a given Task(see Figure 4). The method that allows you to react to a Task once it completes is ContinueWith().
Figure 4: A Task.ContinueWith() allows you to react to a Task’s completion
Task.ContinueWith() is very polymorphic, with a lot of argument permutations. The one that we are interested in is:
public Task ContinueWith( Action<Task> continuationAction )
The variation shown above means that you can use an anonymous method within Task.ContinueWith(). Listing 6 shows an anonymous method within task.ContinueWith((t) => {....}.
var messenger = new DownAndDirtyMessenger(); Task<MessengerResult> task = messenger.SendMessageEx("This is a secret message"); task.ContinueWith((t) => { var fmt = "H:mm:ss:fff"; Console.WriteLine("Message:{0} from within the Tasks, Time in: {1}, Time out: {2}, Status: {3}", t.Result.Message, t.Result.ReceivedTime.ToString(fmt), t.Result.SendTime.ToString(fmt), t.Result.SendStatus.ToString()); });
Listing 6: Using an anonymous method in Task.ContinueWith();
Notice, please, that the t passed as a parameter in the lambda expression represents the Task associated with the method, ContinueWith(). Because we can get at the Task, we can get the to TResult by way of the property, t.Result.
Now, here is where it gets really interesting. Please remember that, in Listing 6, DownAndDirtyMessenger.SendMessageEx() returns a Task<MessengerResult>. The TResult of the Task returned by the method is type, MessengerResult. Thus, the type of the property, t.Result is MessageResult. We access the properties of the type, MessageResult, as follows:
t.Result.ReceivedTime.ToString(fmt), t.Result.SendTime.ToString(fmt) t.Result.SendStatus.ToString());
The important thing to take away from all this is that Task.ContinueWith() allows you to react to a Task upon completion and that we can use a custom class as a TResult to get result information from a Task.
Iteration with Parallel Loops
The Task Parallel Library contains the class, Parallel. Parallel allows you to do asynchronous looping. We’re going to look at three methods of the Parallel class.
- Parallel.Invoke()
- Parallel.For()
- Parallel.ForEach()
Each of these methods is highly polymorphic, with lots of parameterization. So for now, we’re going to take a simple look at using each. After all, this is a Dirty and Dirty article, not a Deep and In-Depth.
Parallel.Invoke()
Parallel.Invoke() allows you to invoke methods asynchronously. Listing 7 shows you how to use Parallel.Invoke() to execute five methods simultaneously. Parallel.Invoke() is wrapped up in a method, SendSpam().
private string _fmt = "H:mm:ss:fff"; private void SimpleSend(string message) { Console.WriteLine(message + " at " + DateTime.Now.ToString(_fmt)); } public void SendSpam() { Parallel.Invoke( () => SimpleSend("Moe"), () => SimpleSend("Larry"), () => SimpleSend("Curly"), () => SimpleSend("Shemp"), () => SimpleSend("Joe Besser")); }
Listing 7: Parallel.Invoke takes an array of lambda expressions asynchronously
We call SendSpam() as follows:
var messenger = new DownAndDirtyMessenger(); messenger.SendSpam();
The return of the call is a series of Console.Write statements, the output of which is shown below.
Moe at 15:34:39:478 Larry at 15:34:39:482 Curly at 15:34:39:482 Shemp at 15:34:39:482 Joe Besser at 15:34:39:483
Notice that the timestamps of the line of output indicate that the run was almost simultaneous. Yes, we have two outliers. The machine that this code is running on is a two-core machine. So, we might chalk up the slowness of the two outliers to have maxed out the capacity of the machine’s CPU.
Parallel.For()
Parallel.For() allows you to run items items in a for loop asynchronously. The syntax of the Parallel.For() statement is similar to a standard for loop, except the incrementer is implied, so that no i++ is needed. Listing 8 shows you Parallel.For() in action.
public void SendMessages(string[] messages) { Parallel.For( 0, messages.Length, i => { SimpleSend(messages[i]); }); }
Listing 8: Parallel.For() allows you to iterate over an array asynchronously
The Parallel.For() in Listing 8 is wrapped in a method, SendMessages(string[] messages).
Here is an example of calling the Parallel.For() by way of SendMessages(string[] messages).
var messenger = new DownAndDirtyMessenger(); string[] messages = { "Hello There!", "Goodby There!", "Do you know the meaning of life?" }; messenger.SendMessages(messages);
The outputs are shown below. Notice again that the method, SimpleSend(…), which is called in the Parallel.For executes almost simultaneously. The lag is probably due to my funky, old, two-core machine.
Hello There! at 15:36:51:401 Goodby There! at 15:36:51: Do you know the meaning of life? at 15:36:51:405
Parallel.ForEach()
Parallel.ForEach allows you to iterate through collections. Listing 9 shows SendMessages(IEnumerable<string> messages), which is a wrapper for Parallel.ForEach(...).
public void SendMessages(IEnumerable<string> messages) { Parallel.ForEach(messages, message => { Console.WriteLine(message + " at " + DateTime.Now.ToString(_fmt)); }); }
Listing 9: Parallel.ForEach(…) iterates over collections.
We call SendMessage like so:
var messenger = new DownAndDirtyMessenger(); IEnumerable<string> messages = new List<string> { "Hi Moe", "Hi Larry", "Hi Curly", "Hi Shemp", "Hi Joe Besser" }; messenger.SendMessages(messages);
And the output is shown below:
Hi Moe at 15:39:19:682 Hi Larry at 15:39:19:685 Hi Curly at 15:39:19:686 Hi Shemp at 15:39:19:686 Hi Joe Besser at 15:39:19:686
Again, near simultaneous calls, weirdness due to my machine.
Putting It All Together
This has been a typical Down and Dirty Session. We’ve covered a lot. You learned how to get up and running using the Task object and you looked at using the Parallel class to iterate asynchronously.
There is little doubt about it, the Task Parallel Library makes asynchronous programming in .NET a whole lot easier than it was in years prior. Still, the technology is broad. You’ll need some time get accustomed to it. I hope that you give yourself the time because, once you do, a whole new way of thinking in code will open up to you.
Get the Code
The code for this article consists of a .NET solution that contains the DownAndDirtyMessenger demo project and a unit test project that runs the DownAndDirtyMessenger project discussed in this article. You can get the code from the Download link at the bottom of this article.
Learn More
If you want more about TPL and parallelization, check out Microsoft’s three-part series on Channel 9.