Microsoft .NET Framework 4.0 Task Parallel Library Continuations

Introduction

If you’ve been following my Task Parallel Library (TPL) articles you’re probably already aware that Microsoft has made a hefty investment in Parallel Programming. An earlier article covered the task class, Understanding Tasks in the .NET Framework 4.0 Task Parallel Library. Continuations, a term for chaining tasks together, was briefly mentioned in the article, yet the topic is key to exploiting tasks. Using some sample applications I’m going to demonstrate how to compose continuations.

Overview

Continuations have similarities to callback functions in asynchronous programming. Like a callback a developer attaches a delegate or lambda function to a data structure. The code is then invoked by the particular infrastructure being tapped in the Framework. Usually the code is called in response to some event.

Unlike callbacks, continuations are much easier to write, more flexible to use, and cover a wider range of usage scenarios. Like most things in the Task Parallel Library, continuations leverage tasks. I always think of tasks as little modules of work. Developers utilizing the Task Parallel Library (TPL) break their program into tasks and submit tasks for execution. Tasks are the center of TPL. Continuations are one of many ways to compose groups of tasks.

Continuations work like this:

  • A Parent Task is created.
  • Utilizing methods on the task class; a delegate or lambda expression is assigned to execute upon completion of a parent or more accurately an antecedent task.
  • The task class methods wrap the delegate or lambda in another task class.
  • TPL executes the parent task.
  • When the parent task completes TPL executes the continuation.

Methods attached to the task class make structuring continuations easy. I’ve developed three sample applications that demonstrate continuations. Two of the samples demonstrate more advanced continuation options, but the first demonstrates the basics.

The Basics

Below is the full code for the first sample.

  static void Main(string[] args)
  {
      var taskMain = new Task<string>(() =>
      {
          return "TaskMain succeeded";
      }
      );

      var taskContinue = taskMain.ContinueWith<int>((t) =>
          {
              Console.WriteLine("t.Result is " + t.Result);

              return t.Result.Length;
          }
      );

      taskMain.Start();
      taskContinue.Wait();

      Console.WriteLine("The continuation returned " + taskContinue.Result.ToString());
      Console.WriteLine("Press any key to quit...");
      Console.ReadKey();
  }

In the sample above, taskMain returns a string. ContinueWith assigns a block of code that receives a Task<string> class and returns the length of the string. It may appear that the relationship between the task is a parent-child one, however, as you’ll see in later samples; that assumption is not entirely accurate. The relationship is peer to peer with the second task dependent on the completion of the first, but no more than that.

In fact, as you may have noticed, the sample starts taskMain, but waits for the completion of taskContinue. This was a trivial example involving a couple of classes. A more complicated sample involves far more tasks.

Multiple Tasks

A more complicated sample involving multiple tasks appears below.

  static void Main(string[] args)
  {
      var pause = new Action<string>((msg) =>
          {
              Console.WriteLine(msg);
              Console.WriteLine("Press any key to continue...");
              Console.ReadKey();
          }
      );

      var taskMain = new Task<string>(() =>
      {
          pause("Executed taskMain");
          return "TaskMain succeeded";
      }
      );

      var taskContinue = taskMain.ContinueWith((t) =>
      {
          pause("Executed taskContinue");

          return "TaskContinue succeeded";
      }
      );

      Task<string>[] taskList = new Task<string>[2];
      taskList[0] = new Task<string>(() =>
          {
              pause("Executed 0");
              return "Executed 0";
          }
      );

      taskList[1] = new Task<string>(() =>
      {
          pause("Executed 1");
          return "Executed 1";
      }
      );

      var taskFinally = taskContinue.ContinueWith((t) =>
      {
                  
          pause("Executed finally");

          if (t.Result == "TaskContinue succeeded")
          {

              taskList[0].Start();
              taskList[1].Start();
          }

          return "TaskFinally";
      }
      );


      taskMain.Start();

      Task.WaitAll(taskList);

  }

This sample demonstrates how a number of tasks can be strung together in a solution with many levels and complicated branching. Like the prior example this example inspects the result of a prior task. However, in this example, additional tasks execute when conditions on the result are met.

Again, this example utilizes lambda expressions. It also shows how local variables can be used inside the lambda expression and how data structures like actions (delegates) can pass code blocks throughout an application. Similar ideas can be applied to a WPF application or a WCF Service. Handing blocks of code to tasks and passing the results from one task to another is the underpinning for a responsive application.

A final example demonstrates exception handling and how to create relationships between tasks.

More by Author

Must Read