Click here to Skip to main content
Click here to Skip to main content

Tasks/Continuations and Death of the ThreadPool?

, 15 Aug 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
Using the VS2010 Tasks namespace.

Contents

Introduction

This article explores the new VS2010 System.Threading.Tasks namespace that has come about due to the ongoing work that is being done with the Microsoft Parallel Extensions to .NET Framework 3.5, June 2008 Community Technology Preview.

As this article is using this namespace, it obviously requires VS2010, so if you haven't got that, sorry but the reading is over I guess. However, if you are still curious, carry on, and let's have a look at what this new namespace can do for us.

Currently

The current support we have for running small work items of code on a background thread, is really to use the System.Threading.ThreadPool, which I have to say I am a real fan of. I actually discuss ThreadPool in detail in one of my old threading articles, should you be interested:

The reason I like the ThreadPool is that the ThreadPool does all the nasty thread management stuff for me, and schedules my work item for me when it has time to run it. You would currently use the ThreadPool something like this:

using System;
using System.Threading;

namespace QueueUserWorkItemWithState
{
    // ReportData holds state information for a task that will be
    // executed by a ThreadPool thread.
    public class ReportData
    {
        public int reportID;

        public ReportData(int reportID)
        {
            this.reportID = reportID;
        }
    }

    /// <summary>
    /// Queues a method for execution with a ReportData state object
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {

            // Queue the task with a state object
            ReportData rd = new ReportData(15);
            ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), rd);

            Console.WriteLine("Main thread does some work, then sleeps.");
            // If you comment out the Sleep, the main thread exits before
            // the thread pool task runs. The thread pool uses background
            // threads, which do not keep the application running. (This
            // is a simple example of a race condition.)
            Thread.Sleep(1000);

            Console.WriteLine("Main thread exits.");
            Console.ReadLine();

        }

        // This thread procedure performs the task.
        static void ThreadProc(Object stateInfo)
        {
            //get the state object
            ReportData rd = (ReportData)stateInfo;
            Console.WriteLine(string.Format("report {0} is running", rd.reportID));
        }
    }
}

Which is, of course, a much better solution than managing our own threads. However, there are several problems with the current ThreadPool object, such as:

  • Work items can't return a value (however BeginInvoke/EndInvoke delegates do return values)
  • Can not cancel tasks
  • No waiting on work items, unless you use external synchronization methods such as WaitHandles etc.
  • Composing a group of items in a structured way
  • Handling exceptions thrown concurrently or any other richer construct built on top of it

In fact, the way the current ThreadPool works is, you just chuck some work at it and forget about it. Hmmm, surely there is a better way.

In the past, I would have remedied this by using the absolutely rock solid and brilliant SmartThreadPool by Ami Bar, which fixes these and many other issues, but today, I had a bit of time on my hands, so I thought I would look into the new VS2010 System.Threading.Tasks namespace and see what it had to offer.

And I have to say, from what I have seen, it looks like it will offer a lot; let's carry on and have a look, shall we?

Death of a Legend: Tasks

Before we start, let me just mention the demo app. The demo app is a Visual Studio 2010 WPF project, so you will need VS2010 to run it.

When run, it looks like this, and the basic idea is dead simple: run some background Task that fills the ListBox with data items. That is it.

Here is a screenshot of the demo app in all its glory; pretty, ain't she?

As can be seen from the demo app screenshot above, there are seven buttons, which seem to do different things. So what do these seven buttons actually do? Well, the buttons do the following:

  • Run a Task with no state passed to the Task
  • Run a Task with state passed to the Task
  • Create and run a Task using the TaskFactory which returns a result
  • Create and run a Task, which uses a Continuation
  • Start a canceallable Task
  • Cancel a canceallable Task
  • A weird one, is it broken, or not?

So I think the best way to carry on is to look at each of these seven areas one by one. So let's proceed to do that.

Run a Task With No State Passed to the Task

This is by far the easiest setup to use, as all we really need to do is create a Task and Start it. We do this quite easily as the Tasks constructor takes a Action (delegate) which is a pointer to the Task's payload that will be run when the Task is started. The example below shows this, and it should be noted that the UI thread will simply continue and will not block waiting for the Task, so this is how you might setup a background activity. It should also be noted that by using the Task class, it is not possible to obtain a return value. It would, however, be possible if we use the Task<T>, which we shall cover in a while.

/// <summary>
/// This handler shows you to use a Task that doesn't use any 
/// state inside the Task
/// 
/// This task runs Asycnhronously and doesn't block the calling Thread
/// </summary>
private void TaskNoState_Click(object sender, RoutedEventArgs e)
{
    Task task = new Task((Action)DoSomething);
    items.Clear();
    task.Start(); //NOTE : No Wait so runs Asynch
    lstItems.ItemsSource = items;
    MessageBox.Show("TaskNoState DONE");
}

/// <summary>
/// Runs the Tasks action delegate, accepting the NO state from the task
/// </summary>
private void DoSomething()
{
    StringBuilder build = new StringBuilder();
    build.AppendLine("This simple method demostrates how to use a Task with no state");
    build.AppendLine("That means you can only really do something that doesn't");
    build.AppendLine("need an input value, and doesn't return anything");
    MessageBox.Show(build.ToString());
}

Run a Task With State Passed to the Task

This time, all that we are doing is passing a single Object which represents the Task state data to the actual Task. Otherwise, it is the same arrangement as the example shown above; we are still just using an Action delegate. The difference this time is that the Action delegate is of type Action<T>, so it can accept a parameter. Here is an example of setting up a Task to accept some Object state. As before, the example below allows the UI thread to simply continue, and will not block waiting for the Task, so this is how you might setup a background activity.

/// <summary>
/// This handler shows you to use a Task that accepts some input
/// state that can then be used inside the Task
/// 
/// This task runs Asycnhronously and doesn't block the calling Thread
/// </summary>
private void TaskState_Click(object sender, RoutedEventArgs e)
{
    Task task = new Task((Action<Object>)CreateStuffForTaskState,
    new ObjectState<Int32>
    {
        CurrentTask = "TaskState",
        Value = 999999
    });
    items.Clear();
    task.Start(); //NOTE : No Wait so runs Asynch
    items = new ObservableCollection<string>(taskItems);
    lstItems.ItemsSource = items;
    MessageBox.Show("TaskState DONE");
}

/// <summary>
/// Runs the Tasks Action delegate, accepting the state that
/// was created for the task
/// </summary>
/// <param name="o">the Task state</param>
private void CreateStuffForTaskState(Object o)
{
    var state = o as ObjectState<Int32>;
    taskItems.Clear();
    for (int i = 0; i < state.Value; i++)
    {

        taskItems.Add(String.Format("{0} Item {1}", state.CurrentTask, i));
    }
}

In the example above, the state data is represented by this simple class, which is used anywhere where a Task State is used in the demo app.

/// <summary>
/// Simple Task state object
/// </summary>
/// <typeparam name="T">The type to use</typeparam>
internal class ObjectState<T>
{
    public String CurrentTask { get; set; }
    public T Value { get; set; }

}

Create and Run a Task Using the TaskFactory Which Returns a Result

So far, we have seen how to queue up delegates within Tasks that get scheduled to run, which is all cool, but sometimes, we need a return result from our background activity, say if the background activity was to fetch the next 10,000 records from the database. For this operation, we may actually need a return value. So, can the System.Threading.Tasks namespace deal with return values? The answer is yes, it sure can. All we need to do with the Tasks is set them up using the generic Task<T> type and use some Func<T> delegates as the payload to run for the Task<T>.

Here is an example; also note that in this example, I am using the TaskFactory for creating and starting a new Task; I think this is the preferred method.

/// <summary>
/// This handler shows you to use a Task that doesn't use any 
/// state inside the Task
/// 
/// This Task blocks the calling thread until the Task completes
/// </summary>
private void TaskFactWithReturnValue_Click(object sender, RoutedEventArgs e)
{
    Func<ObservableCollection<String>> obtainTaskResults = TaskWithResults;
    Task<ObservableCollection<String>> task =
        Task.Factory.StartNew<ObservableCollection<String>>(obtainTaskResults,
        TaskCreationOptions.DetachedFromParent);
    items.Clear();
    task.Wait(); //Blocks while waiting for Task to complete
    items = task.Result;
    lstItems.ItemsSource = items;
    MessageBox.Show("TaskFactWithReturnValue DONE");
}

/// <summary>
/// Runs the Tasks Func delegate, which returns a list
/// of ObservableCollection String
/// </summary>
private ObservableCollection<String> TaskWithResults()
{
    ObservableCollection<String> results = new ObservableCollection<string>();
    for (int i = 0; i < 10; i++)
    {
        results.Add(String.Format("TaskFactWithReturnValue Item {0}", i));
    }
    return results;
}

You can see in this example above, as we actually want a return value, I am waiting for the Task to complete using the Task.Wait() method which blocks the UI thread, and then I am using Task.Result to update the UI. I do not like the fact that the UI thread is blocked, but I can not see what you can do apart from wait if you are needing the results. TaskWait() does, of course, have an overload that can accept a TimeSpan timeout, so we could alleviate this waiting by doing something like task.Wait(2500), which waits for 2.5 seconds only.

Create and Run a Task, Which Uses a Continuation

So we have just seen that we can create a Task that returns a value, which is really nice and not something that the current ThreadPool does. What else can these Tasks do? Well, they have another trick or two up their sleeves; one such trick is something called a Continue, which is essentially another Task to run after the current Task completes. By using Tasks and Continuations, you can sequence Tasks in a certain order without the use of external WaitHandles such as ManualResetEvent/AutoResetEvent.

This is easily achieved using the Task.ContinueWith<T>() method, as shown below.

Here is an example that creates two Tasks that go into forming one final set of result values that are then shown on the UI:

/// <summary>
/// This handler shows you to use a Task that will use 2 chained Tasks
/// the first Task accepts some State, and the 2nd Task in the chain doesn't
/// ONLY when both Tasks have completed is the work considered done
/// 
/// This Task blocks the calling thread until the 2 chained Task complete
/// </summary>
private void TaskContinue_Click(object sender, RoutedEventArgs e)
{
    //SYNTAX EXAMPLE 1

    #region SYNTAX EXAMPLE 1 (Comment to try SYNTAX EXAMPLE 2)
    Func<Object, ObservableCollection<String>> obtainTaskResultsFunc = 
                                                         TaskWithResultsWithState;
    Task<ObservableCollection<String>> task =
        Task.Factory.StartNew(obtainTaskResultsFunc, new ObjectState<Int32>
        {
            CurrentTask = "TaskState",
            Value = 2
        });

    Func<Task, ObservableCollection<String>> contineResultsFunc = 
                                                              ContinueResults;
    Task<ObservableCollection<String>> continuationTask =
        task.ContinueWith<ObservableCollection<String>>(contineResultsFunc,
        TaskContinuationOptions.OnlyOnRanToCompletion);

    continuationTask.Wait();
    items.Clear();
    items = continuationTask.Result;
    #endregion

    //SYNTAX EXAMPLE 2
    #region SYNTAX EXAMPLE 2 (UnComment to try And Comment SYNTAX EXAMPLE 1)
    //Task<ObservableCollection<String>> taskAll =
    //    Task.Factory.StartNew((o) =>
    //        {
    //            return TaskWithResultsWithState(o);
    //        }, new ObjectState<Int32>
    //            {
    //                CurrentTask = "TaskState",
    //                Value = 2
    //            }).ContinueWith<ObservableCollection<String>>((previousTask) =>
    //                {
    //                    return ContinueResults(previousTask);
    //                },TaskContinuationOptions.OnlyOnRanToCompletion);


    //taskAll.Wait(); 
    //items.Clear();
    //items = taskAll.Result;
    #endregion


    lstItems.ItemsSource = items;
    MessageBox.Show("TaskContinue DONE");
}

This would produce something like this in the UI when run:

It can be seen that the first two results came from the original Task, and the rest came from the Continuation Task that was only started when the first Task completed.

Create a Task and Cancel It

Another advertised nice feature of Tasks is that they can be cancelled. A Task can be requested to be cancelled, but as a Task is actually just a delegate that is running, there needs to be some co-operation in the Task's payload delegate, which inspects certain aspects about the running Task and takes the correct route based on the Task's current settings.

Here is an example where the Task is extremely low running, and the UI doesn't block and the Task is running in the background, but the user may cancel the Task. Obviously, the work that is done in the Task's payload delegate must make sure that it knows what to do with a cancellation request, and act accordingly.

/// <summary>
/// This handler shows you to use a Task that
/// you can run, a wait a certain amount of time
/// for, and then cancel the Task
/// 
/// This task runs Asycnhronously and doesn't block the calling Thread
/// </summary>
private void TaskCancelStart_Click(object sender, RoutedEventArgs e)
{
    if (cancelableTask != null && 
                cancelableTask.Status == TaskStatus.Running)
        return;

    try
    {
        cancelableTask = new Task((Action)DoSomethingWithCancel);
        items.Clear();
        cancelableTask.Start();

        items = new ObservableCollection<string>(taskItems);
        lstItems.ItemsSource = items;
    }
    catch (Exception ex)
    {

        MessageBox.Show(String.Format("TaskCancel Exception {0}", 
                        ex.InnerException != null ? " : " + 
                        ex.InnerException.Message : String.Empty));
    }
}

/// <summary>
/// Cancels the Tasks (requests in to Cancel)
/// </summary>
private void TaskCancelStop_Click(object sender, RoutedEventArgs e)
{
    cancelableTask.Cancel();
}

/// <summary>
/// Runs the Tasks Action delegate, but will examine if the Task has been cancelled
/// and if it has the code in this delegate will accept the Cancellation request and
/// transition the Task to the Cancelled state
/// </summary>
private void DoSomethingWithCancel()
{
    taskItems.Clear();
    for (int i = 0; i < Int32.MaxValue; i++)
    {

        //See if the current Task is cancelled,
        //and if so get this delegate to acknowledge
        //the cancellation
        if (!cancelableTask.IsCancellationRequested)
            taskItems.Add(String.Format("TaskCancel Item {0}", i));
        else
        {
            //transition Task to Cancelled state
            Task.Current.AcknowledgeCancellation();
            break;
        }

    }
}

It can be seen that there is a Task.Cancel() method which can be used to request a Task cancellation, and there is also a IsCancellationRequested property which can be inspected to see if a cancellation has been requested. From there, the Task's payload delegate must play fair and acknowledge the cancellation by using the Task.AcknowledgeCancellation() method.

A Weird One, Not All Plain Sailing

While I am quite impressed by the new Task functionality, I did do some experimenting, such as running this bit of code using a Task<T> which, as we now know, should return a result.

/// <summary>
/// This seems to be pretty broken to me, I could be misunderstanding something but
/// I thought Tasks were using new CLR4.0 ThreadPool as described 
/// http://www.danielmoth.com/Blog/2008/11/new-and-improved-clr-4-thread-pool.html
/// which should cause a new thread to do this work on a new worker thread, so why
/// when this code is run is the UI unresponsive
/// </summary>
private void IsThisBroken_Click(object sender, RoutedEventArgs e)
{
    Func<ObservableCollection<String>> obtainTaskResults = 
                                                   TaskWithResultsWhyIsItBlockingUI;
    Task<ObservableCollection<String>> task =
        Task.Factory.StartNew<ObservableCollection<String>>(obtainTaskResults,
        TaskCreationOptions.DetachedFromParent);
    items.Clear();
    //THE UI is simply queing this work up, and it not waiting (its commented out below)
    //So why is the UI Not reponsive at all

    //task.Wait(); //Blocks while waiting for Task to complete
    items = task.Result;
    lstItems.ItemsSource = items;
    MessageBox.Show("IsThisBroken DONE");
}

/// <summary>
/// Runs the Tasks Func delegate, which returns a list
/// of ObservableCollection String
/// </summary>
private ObservableCollection<String> TaskWithResultsWhyIsItBlockingUI()
{
    ObservableCollection<String> results = new ObservableCollection<string>();
    //*************************************************
    //      VERY LONG RUNNING TASK, CAUSES UI TO BLOCK
    //      SEEMS TO BE BLOCKING WAITING ON Task<T>.Result
    //      basically the following line above
    //
    //          items = task.Result;
    //*************************************************
    for (int i = 0; i < Int32.MaxValue; i++)
    {
        results.Add(String.Format("TaskFactWithReturnValue Item {0}", i));
    }
    return results;
}

Now when I run this bit of code, exactly as it is shown, the UI becomes unresponsive, which puzzled me slightly as we are not using Task.Wait() (it is commented out above) which would block the calling Thread until the Task is completed, but the UI is clearly blocking or is not responsive. Try the demo for yourself. Using the demo code, keep the Task<T> using the Int32.MaxValue and put a breakpoint on the line in the IsThisBroken_Click() method of the demo app.

items = task.Result;

So I started looking into this a bit more, and I thought I wonder if a Task that returns something using the Task<T>.Result property blocks the caller until the Task<T> is completed and has a return value.

So what I did was:

  1. Comment out the two lines above dealing with the items, so these lines now became:
  2. //task.Wait(); //Blocks while waiting for Task to complete
    //items = task.Result;
    //lstItems.ItemsSource = items;
    MessageBox.Show("IsThisBroken DONE");

    And guess what, the UI was responsive straight away, though I have now lost the ability to use the Task<T> return value obviously.

  3. So I then put the original code back in, and just decreased the payload of the Task<T> so it used a shorter loop value; so instead of Int32.MaxValue, I used 50,000, and then too, I saw that the code jumped straight back to a breakpoint that I set on the line:
  4. items = task.Result;

This kind of proved to me that there appears to be a blocking action that the Task<T>.Result property enforces. This actually makes sense when you think about it, but it is something to be aware of. I think a good way to keep track of long running Tasks would be to periodically check the Task status using the TaskStatus enum, where you could use something like TaskStatus.RanToCompletion. All this said, Tasks do provide quite a nice API to use, and I think they are a bit better than the ThreadPool API we currently have.

Other useful links about Tasks:

Task related links at the Task Parallel Library site:

That's It, Hope You Liked It

That is all I think I wanted to say about Tasks, but I am sure you will agree, these look pretty good, and are a welcome addition to the current System.Threading namespace.

Thanks

As always, votes / comments are welcome.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralIsn’t up to date PinmemberAndrey Piskov22-Feb-10 13:10 
GeneralRe: Isn’t up to date PinmvpSacha Barber22-Feb-10 20:54 
This is not the best way to ask someone to do something. Remember authors here do this in there spare time, so just chill out man.
Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue
 
My Blog : sachabarber.net

GeneralRe: Isn’t up to date PinmemberAndrey Piskov22-Feb-10 23:55 
GeneralRe: Isn’t up to date PinmemberFredrik Bornander23-Feb-10 1:10 
GeneralRe: Isn’t up to date PinmvpSacha Barber23-Feb-10 2:34 
GeneralAnother 5 PinmemberRaul Mainardi Neto18-Aug-09 3:09 
GeneralRe: Another 5 PinmvpSacha Barber18-Aug-09 4:00 
GeneralItems<t>.Result</t> Pinmemberstixoffire17-Aug-09 21:48 
GeneralRe: Items.Result PinmvpSacha Barber17-Aug-09 22:36 
Generalout of box question Pinmemberwasimsharp17-Aug-09 21:29 
GeneralRe: out of box question PinmvpSacha Barber17-Aug-09 22:35 
GeneralRe: out of box question Pinmemberwasimsharp18-Aug-09 1:15 
AnswerNice PinmemberHristo Bojilov16-Aug-09 4:31 
GeneralRe: Nice PinmvpSacha Barber16-Aug-09 4:49 
GeneralTypo Pinmemberaspdotnetdev15-Aug-09 16:42 
GeneralRe: Typo PinmvpSacha Barber15-Aug-09 21:47 
GeneralRe: Typo PinmemberJason Barry17-Aug-09 9:07 
GeneralRe: Typo PinmvpSacha Barber17-Aug-09 22:37 
GeneralThanks Pinmembergpgemini15-Aug-09 12:00 
GeneralRe: Thanks PinmvpSacha Barber15-Aug-09 21:47 
GeneralRe: Thanks Pinmemberaspdotnetdev15-Aug-09 21:52 
GeneralRe: Thanks PinmvpSacha Barber15-Aug-09 23:23 
GeneralRe: Thanks PinmemberS. Senthil Kumar16-Aug-09 7:01 
GeneralRe: Thanks PinmvpSacha Barber16-Aug-09 22:42 
GeneralRe: Thanks PinmemberBrowniePoints17-Aug-09 10:51 
GeneralRe: Thanks PinmemberS. Senthil Kumar17-Aug-09 18:52 
GeneralRe: Thanks PinmvpSacha Barber17-Aug-09 22:33 
GeneralRe: Thanks PinmvpSacha Barber17-Aug-09 22:32 
RantRe: Thanks PinmemberMikael Eliasson16-Aug-09 22:42 
GeneralRe: Thanks PinmemberWilliam E. Kempf17-Aug-09 6:16 
GeneralRe: Thanks PinmvpSacha Barber17-Aug-09 6:28 
GeneralRe: Thanks PinmemberErik Funkenbusch6-Jun-10 10:43 
GeneralRe: Thanks PinmvpSacha Barber6-Jun-10 11:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141220.1 | Last Updated 15 Aug 2009
Article Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid