Mechanisms for Creating Tasks

Mechanisms for Creating Tasks

  • Comments 16

The core entity in the Task Parallel Library around which everything else revolves is System.Threading.Tasks.Task.  The most common way of creating a Task will be through the StartNew method on the TaskFactory class, a default instance of which is exposed through a static property on Task, e.g.

var t = Task.Factory.StartNew(() =>
{
    … // body of the task goes here
});

There are, however, other ways of creating tasks.  For example, while using StartNew is the preferred mechanism to create a Task and schedule/start it, we do support separating those two operations into two discrete actions, e.g.

var t = new Task(() =>
{
    … // body of the task goes here
});
… // the task has been created but not scheduled
t.Start(); // now schedule it

Moreover, StartNew isn’t alone on TaskFactory; other methods like ContinueWhenAll, ContinueWhenAny, and FromAsync may all be used to create Task instances. Task also exposes a ContinueWith mechanism that can be used to create a Task that will be scheduled when the antecedent task (the Task on which ContinueWith is being called) completes.

Finally, the TaskCompletionSource<TResult> type can be used to create a Task<TResult> completely controlled by the completion source instance, through its SetResult, SetException, and SetCanceled methods (and their TrySet* variants).

Each of these different ways of creating a Task has different behaviors associated with it.  The differences between them may not be obvious at first, but with a little thought, it should be clear why things behave the way they do.  For example, calling Start on a Task created by StartNew is invalid (i.e. results in an exception)… you can’t start an already started Task.  In contrast, a Task created by a Task’s constructor won’t have been scheduled, so it’s perfectly valid to call Start on it.  Calling Start on a Task returned by a TaskCompletionSource<TResult> makes little sense, as there’s nothing to “start”, so that’s invalid.  It’s invalid to call Start on a continuation task (e.g. one created by ContinueWith, ContinueWhenAny, or ContinueWhenAll) because the work should only be scheduled when the antecedent(s) has completed.  And it’s invalid to call Start on a task created by FromAsync, because the work being done has already been initiated through a call to the beginMethod passed to FromAsync.

These kinds of behavioral differences can be quite useful when building up abstractions on top of tasks.  For example, let’s say I want to implement a factory method for creating “delayed” tasks, ones that won’t actually be scheduled until some user-supplied timeout has occurred.  One way to write this would be as follows:

public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
    // Validate arguments
    if (millisecondsDelay < 0)
        throw new ArgumentOutOfRangeException("millisecondsDelay");
    if (action == null) throw new ArgumentNullException("action");

    // Create the task
    var t = new Task(action);
    // Start a timer that will trigger it
    var timer = new Timer(
        _ => t.Start(), null, millisecondsDelay, Timeout.Infinite); 
    t.ContinueWith(_ => timer.Dispose());
    return t;
}

This implementation creates a new Task to run the provided action, but doesn’t immediately start it.  Instead, it creates a Timer with the user-supplied delay, and when the timer expires, the Timer’s callback starts the task.  Once the timer has been started, the Task is returned to the user.

One problem with this implementation, which you might have guess based on earlier paragraphs, is that the Task returned to the user was created using the Task’s constructor.  This means it can be explicitly Start’d.  And that means the Task returned from StartNewDelayed could be started by the consumer prior to the Timer firing.  That’s bad for two reasons: one, it breaks expectations about the behavior of the Task and the associated delay, and two, a Task may only be started once.  If the Task is explicitly started and then the timer’s callback tries to Start the Task, kaboom: Start will throw an exception (since the Task was already started), the exception will go unhandled, and the app will come crumbling down.

Given what we now know about behaviors associated with creating tasks, we can use a different mechanism for creating a task that doesn’t allow the Task to be explicitly started.

public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
    // Validate arguments
    if (millisecondsDelay < 0)
        throw new ArgumentOutOfRangeException("millisecondsDelay");
    if (action == null) throw new ArgumentNullException("action");

    // Create a trigger used to start the task
    var tcs = new TaskCompletionSource<object>();

    // Start a timer that will trigger it
    var timer = new Timer(
        _ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite);

    // Create and return a task that will be scheduled when the trigger fires.
    return tcs.Task.ContinueWith(_ =>
    {
        timer.Dispose();
        action();
    });
}

In this new implementation, I’ve taken advantage of the fact that a continuation task can’t be explicitly started and can be used to run arbitrary user code.  The timer is used to resolve a TaskCompletionSource<TResult>, and a continuation off of that completion source is used to run the action.  It’s that continuation that’s returned.

Leave a Comment
  • Please add 5 and 8 and type the answer here:
  • Post
  • Thank you for submitting this cool story - Trackback from DotNetShoutout

  • PingBack from http://blog.cwa.me.uk/2009/06/04/the-morning-brew-361/

  • hello :)

    maybe this is a silly question but why not use Thread.Sleep()?

    ex:

    return Task.StartNew(()=> {

    Thread.Sleep(delay);

    action();

    });

    is there a perf implication to that approach?

  • Hi aL-

    Not a silly question, but as you guessed there are performance implications to doing what you suggest.  The code you've shown will renders that task's thread useless for executing work while it's sleeping.  Thus in order to make forward progress on work elsewhere, another thread will need to be injected in order to keep the CPUs busy; that injection takes time and also results in extra resource utilization, such as by default a megabyte of memory for the stack space.  With the timer approach, work is only actually scheduled when the work is ready to be done, rather than blocking additional threads.

    Does that help?

  • yes:) i infact thought Thread.Sleep() released the thread back to the threadpool.. shows what i know ;) [now that i think about it that seems unlikely]

    So Thread.Sleep() is just spinning the thread until the time limit is hit?  

  • Thread.Sleep doesn't spin.  Rather, it tells the OS not to schedule the thread for execution until the relevant time period has elapsed.

  • ah i see.. so it does sort of what Timer does but on the OS thread level instead of the Task level? it'd be interesting to hear more about how Timer does its thing :) does it talk to the threadpool directly and asks not to be scheduled until the time has passed? how does it do that? with an internal api?

    sorry, lots of questions.. but the tpl is very interesting stuff :)

  • Timer works by registering itself with the ThreadPool, which inserts the timer in a list of active timers.  The ThreadPool maintains a separate "timer thread" which calls Sleep() with the duration of the timer that will fire next.  Offloading the Sleep to this special thread has two benefits:  first, it frees the worker thread to execute other tasks, and second it allows multiple timers to be processed by a single thread.  So instead of tying up 10 threads with 10 calls to Thread.Sleep, you have a single thread handling all 10 timers.  As each timer fires, the callback is queued back to the worker pool, freeing up the timer thread to process the rest of the timers.

  • PingBack from http://fixmycrediteasily.info/story.php?id=4745

  • PingBack from http://www.vishwatech.com/globalnews/?p=1726

  • You mention "Task also exposes a ContinueWith mechanism that can be used to create a Task that will be scheduled when the antecedent task (the Task on which ContinueWith is being called) completes.", which I see here:

    http://msdn.microsoft.com/en-us/library/dd321262(VS.100).aspx

    However, I do not see anything like this in the June 2008 CTP. Am I correct in thinking it is missing? If so, is there anywhere I can get a more modern library, usable outside a VS2010 VM?

  • Hi James-

    The June 2008 CTP does have ContinueWith methods on Task, so you should be able to use them there.

    The bits have changed significantly since incorporation into .NET 4, so if you can install the .NET Framework 4 Beta, that'll be your best bet for playing with recent bits.

    http://msdn.microsoft.com/en-us/vstudio/dd582936.aspx

  • PingBack from http://firepitidea.info/story.php?id=576

  • In your first code sample, you have a ContinueWith on the Timer object, is this correct?

    Also, not sure what the underscore means, is this just pseudocode or valid c#?

    var timer = new Timer(

           _ => t.Start(), null, millisecondsDelay, Timeout.Infinite);

       timer.ContinueWith(_ => timer.Dispose());

  • Hi Abhijeet-

    re: is this correct?

    Oops, no, it was a typo.  Should have been "t.ContinueWith" rather than "timer.ContinueWith".  I fixed it in the post. Thanks.

    re: is this just pseudocode or valid C#

    In C#, "_" is a valid identifier name.  Since my delegate doesn't need the parameter that's passed in, I've used an "_" to signify that I don't care about it.

Page 1 of 2 (16 items) 12