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.



Microsoft .NET Framework 4.0 Task Parallel Library Continuations

Exceptions

Good Exception handling is important. Every non-trivial application has some form of exception handling. TPL is part of the .NET Framework so it leverages the exception handling mechanisms in the .NET Framework. Unlike exception handling with callbacks though, as you observed in the prior example; many tasks may be involved in an algorithm. TPL accounts for this using some more TPL specific exception classes.

The exception sample code 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 taskParent = new Task<string>(() =>
          {
              var taskMain = new Task<string>(() =>
              {
                  pause("Executed taskMain");
                  return "TaskMain succeeded";
              }
              ,TaskCreationOptions.AttachedToParent);
  
              var taskContinue = taskMain.ContinueWith((t) =>
              {
                  pause("Executed taskContinue");
  
                  return "TaskContinue succeeded";
              }
              ,TaskContinuationOptions.AttachedToParent);
  
              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");
  
                  throw new Exception("Finally failed");
  
                  taskList[0].Start();
                  taskList[1].Start();
  
                  return "TaskFinally";
              }
              ,TaskContinuationOptions.AttachedToParent);
  
              taskMain.Start();
              taskMain.Wait();
  
              return "Done";
          }
      ,TaskCreationOptions.AttachedToParent);
  
      try
      {
          taskParent.Start();
          taskParent.Wait();
      }
      catch (AggregateException ex)
      {
          var msg = "";
  
          foreach( var e in ex.InnerExceptions)
          {
              msg = msg + " " + e.InnerException.Message;
          }
          pause("Exception message was " + msg);
      }
              
  
      pause("taskParent Completed...");
  
  
  }

Most important to notice in this sample is the use of "AttachedToParent" options that creates a parent-child sort of relationship between tasks. By default all tasks are peers. AttachedToParent options can be assigned when a task is created or when a continuation is created. The sample demonstrates both ways.

Creating a parent-child relationship between tasks has the following effect.

A parent class is not completed until all of its child-tasks completes. The example above waits on the parentTask class. In the prior example the wait was on the last rask being executed.

Exceptions occurring in a child rask "bubble-up" to the parent class. Error handling can be confined to handling on the parent task. Like all error handling, care should be taken when structuring the handling. Removing the TaskContinuationOptions.AttachedToParent removes the relationship with the parent task and means the peer task should have its own exception handling.

As I mentioned earlier, the levels of tasks can become quite complex. Some exceptions may involve multiple tasks. So TPL includes the AggregateException class. AggregateException class "rolls-up" the layers of exceptions that can crop-up when many task classes are joined together in a solution.

Conclusion

Task Parallel Library (TPL) Task class are the modules of work at the center of TPL. Task Continuations allow a developer to create interdependencies between tasks. The interdependencies can involve simple execution in response to task completion or aggregated exception handling among tasks.

Resources

MSDN Continuation documentation
When debugging a thrown error, remember to do this

Related Articles





About the Author

Jeffrey Juday

Jeff is a software developer specializing in enterprise application integration solutions utilizing BizTalk, SharePoint, WCF, WF, and SQL Server. Jeff has been developing software with Microsoft tools for more than 15 years in a variety of industries including: military, manufacturing, financial services, management consulting, and computer security. Jeff is a Microsoft BizTalk MVP. Jeff spends his spare time with his wife Sherrill and daughter Alexandra.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

  • Email is the most common communication vehicle used by organizations of all shapes and sizes. Among the billions of email messages sent every day are sensitive information, critical requests, and other essential business data. IT staff bear the burden of ensuring the confidentiality, integrity, and availability of the information contained within the communication. This white paper explores the email security landscape, an assessment of the threats organizations face,  and the building blocks of an effective …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds