Task.Run vs Task.Factory.StartNew

Task.Run vs Task.Factory.StartNew

Rate This
  • Comments 22

In .NET 4, Task.Factory.StartNew was the primary method for scheduling a new task.  Many overloads provided for a highly configurable mechanism, enabling setting options, passing in arbitrary state, enabling cancellation, and even controlling scheduling behaviors.  The flip side of all of this power is complexity.  You need to know when to use which overload, what scheduler to provide, and the like. And “Task.Factory.StartNew” doesn’t exactly roll off the tongue, at least not quickly enough for something that’s used in such primary scenarios as easily offloading work to background processing threads.

So, in the .NET Framework 4.5 Developer Preview, we’ve introduced the new Task.Run method.  This in no way obsoletes Task.Factory.StartNew, but rather should simply be thought of as a quick way to use Task.Factory.StartNew without needing to specify a bunch of parameters.  It’s a shortcut.  In fact, Task.Run is actually implemented in terms of the same logic used for Task.Factory.StartNew, just passing in some default parameters.  When you pass an Action to Task.Run:

Task.Run(someAction);

that’s exactly equivalent to:

Task.Factory.StartNew(someAction,
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

In this way, Task.Run can and should be used for the most common cases of simply offloading some work to be processed on the ThreadPool (what TaskScheduler.Default targets).  That doesn’t mean Task.Factory.StartNew will never again be used; far from it.  Task.Factory.StartNew still has many important (albeit more advanced) uses.  You get to control TaskCreationOptions for how the task behaves.  You get to control the scheduler for where the task should be queued to and run.  You get to use overloads that accept object state, which for performance-sensitive code paths can be used to avoid closures and the corresponding allocations.  For the simple cases, though, Task.Run is your friend.

Task.Run provides eight overloads, to support all combinations of the following:

  1. Task vs Task<TResult>
  2. Cancelable vs non-cancelable
  3. Synchronous vs asynchronous delegate

The first two bullets should be self-explanatory.  For the first bullet, there are overloads that return Task (for operations that don’t have a result) and there are overloads that return Task<TResult> (for operations that have a result of type TResult).  There are also overloads that accept a CancellationToken, which enables the Task Parallel Library (TPL) to transition the task to a Canceled state if cancellation is requested prior to the task beginning its execution.

The third bullet is more interesting, and is directly related to the async language support in C# and Visual Basic in Visual Studio 11.  Let’s consider Task.Factory.StartNew for a moment, as that will help to highlight what this distinction is.  If I write the following call:

var t = Task.Factory.StartNew(() =>
{
    Task inner =Task.Factory.StartNew(() => {});
    return inner;
});

the type of ‘t’ is going to be Task<Task>; the task’s delegate is of type Func<TResult>, TResult in this case is a Task, and thus StartNew is returning a Task<Task>.  Similarly, if I were to change that to be:

var t = Task.Factory.StartNew(() =>
{
    Task<int> inner = Task.Factory.StartNew(() => 42));
    return inner;
});

the type of ‘t’ is now going to be Task<Task<int>>.  The task’s delegate is Func<TResult>, TResult is now Task<int>, and thus StartNew is returning Task<Task<int>>.  Why is this relevant?  Consider now what happens if I write the following:

var t = Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
});

By using the async keyword here, the compiler is going to map this delegate to be a Func<Task<int>>: invoking the delegate will return the Task<int> to represent the eventual completion of this call.  And since the delegate is Func<Task<int>>, TResult is Task<int>, and thus the type of ‘t’ is going to be Task<Task<int>>, not Task<int>.

To handle these kinds of cases, in .NET 4 we introduced the Unwrap method.  Unwrap has two overloads, which are both extensions methods, one on type Task<Task> and one on type Task<Task<TResult>>.  We called this method Unwrap because it, in effect, “unwraps” the inner task that’s returned as the result of the outer task.  Calling Unwrap on a Task<Task> gives you back a new Task (which we often refer to as a proxy) which represents the eventual completion of the inner task.  Similarly, calling Unwrap on a Task<Task<TResult>> gives you back a new Task<TResult> which represents the eventual completion of that inner task. (In both cases, if the outer task is Faulted or Canceled, there is no inner task, since there’s no result from a task that doesn’t run to completion, so the proxy task then represents the state of the outer task.) Going back to the prior example, if I wanted ‘t’ to represent the return value of that inner task (in this case, the value 42), I could write:

var t = Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
}).Unwrap();

The ‘t’ variable will now be of type Task<int>, representing the result of that asynchronous invocation.

Enter Task.Run.  Because we expect it to be so common for folks to want to offload work to the ThreadPool, and for that work to use async/await, we decided to build this unwrapping functionality into Task.Run.  That’s what’s referred to by the third bullet above.  There are overloads of Task.Run that accept Action (for void-returning work), Func<TResult> (for TResult-returning work), Func<Task> (for void-returning async work), and Func<Task<TResult>> (for TResult-returning async work).  Internally, then, Task.Run does the same kind of unwrapping that’s shown with Task.Factory.StartNew above.  So, when I write:

var t = Task.Run(async delegate
{
    await Task.Delay(1000);
    return 42;
});

the type of ‘t’ is Task<int>, and the implementation of this overload of Task.Run is basically equivalent to:

var t = Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000); 
    return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();

As mentioned before, it’s a shortcut.

All of this then means that you can use Task.Run either with either regular lambdas/anonymous methods or with async lambdas/anonymous methods, and the right thing will just happen.  If I wanted to offload this work to the ThreadPool and await its result, e.g.

int result = await Task.Run(async () =>
{
    await Task.Delay(1000);
    return 42;
});

the type of result will be int, just as you’d expect, and approximately one second after this work is invoked, the result variable be set to the value 42.

Interestingly, the new await keyword can almost be thought of as a language equivalent to the Unwrap method.  So, if we return back to our Task.Factory.StartNew example, I could rewrite the last snippet above as follows using Unwrap:

int result = await Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();

or, instead of using Unwrap, I could use a second await:

int result = await await Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

“await await” here is not a typo.  Task.Factory.StartNew is returning a Task<Task<int>>.  Await’ing that Task<Task<int>> returns a Task<int>, and awaiting that Task<int> returns an int.  Fun, right?

Leave a Comment
  • Please add 6 and 1 and type the answer here:
  • Post
  • > Task.Run that accept Action, Func<TResult>, Func<Task>, and Func<Task<TResult>> [...] All of this then means that you can use Task.Run either with either regular lambdas/anonymous methods or with async lambdas/anonymous methods, and the right thing will just happen.  

    Ah, it's a DWIM function. [en.wikipedia.org/.../DWIM].

    Might I ask, what happens if you "var d = new Dictionary<int, Task<int>>(); ... var task4 = await Task.Run(()=>d[4]);"? Will 'task4' be a Task<int> as might be expected? Or will this give you an integer instead? Or perhaps such cases are rare enough as to not warrant any consideration?

    It sounds very useful for most cases though. Thanks for explaining!

  • Hi Aaron-

    In your example, Task.Run(() => d[4]) will map to the Task.Run<TResult>(Func<Task<TResult>>) overload, and thus will return a Task<int>, not a Task<Task<int>>, so awaiting it will give you back an integer, not a task.  

    If you really wanted the Task<int>, you could use Task.Factory.StartNew, which doesn't do the automatic unwrapping. Or you could force the compiler not to see the result as a task, e.g. instead of doing:

       int rv = await Task.Run(() => d[4]);

    you could do:

       Task<int> rv = (Task<int>)await Task.Run(() => (object)d[4]);

    By casting/typing it to object, the compiler will no longer see the result as a Task<TResult>, and thus it won't use the overload that provides the unwrapping behavior.

    Thanks for the good question.

  • Hi Stephen,

    thank you so much for this great illustration of some internals of the TPL in 4.5!

    Best regards

    -Simon

  • Hi Simon- You're welcome.  I'm glad you've found it helpful.

  • Why does the vs2012 debugger behave differently with reguard to the async modifier in the examples below?

    try

    {

    await Task.Run(() => throw new Exception()); // vs breaks here when debugging

    }catch (Exception ex)

    {

    // exception is caught

    }

    try

    {

    await Task.Run(async () => throw new Exception()); // vs does not break here when debugging

    } catch (Exception ex)

    {

    // exception is caught

    }

  • @nick:

    The debugger is alerting you that the exception went unhandled by user code (the exception gets caught by the Task code in mscorlib, which is not user code).  In the async case, the compiler is transforming your code into a state machine which includes a try/catch around the body, and that transformation is still user code, so the exception is not going unhandled by user code.

  • Giving var t = Task<Task<TResult>> t.Unwrap() returns "new Task<TResult> which represents the eventual completion of that inner task".

    At the same time t.Result returns the inner Task<TResult>.

    What is the difference between the two ?

  • @Oleg Subachev: The key difference is that t.Result will block until t has completed, and only then will it hand you back the inner Task<TResult>.  In contrast, t.Unwrap() will immediately give you back a Task<TResult>, without blocking (but since it's not blocking and returns the task to you immediately, it likely won't be the actual inner task, because the outer task likely won't have completed yet, and as such there is no actual inner task yet).

  • Stephen,

    Year and a half later, and this simple article is still helpful.  Thanks for sharing.

  • @Jeremy: Great, I'm glad you've found it helpful!

  • Monads, monads everywhere .... :O

  • A question about Task.Factory.StartNew or Task.Run. If I have a c# function

    public void Foo()

    {

       .....

      Task,Factory.StartNew( () =>

           // do something time-consuming using some local var

       );

       .....

      return

    }

    Does the function returns or it has to wait for the task to finish ? If that is the case, how should I do it ?

  • @nativecpp: The function returns immediately without waiting for your asynchronous work to complete; that work continues to run in the background.  This works because C# lifts to the heap the locals from the function used by the lambda passed to StartNew/Run, so they're not tied to the lifetime of Foo.

  • Hi Stephen,

    Thanks for the quick response. Sorry, my question is somewhat unclear. local var in my example is declared outside the lambda (i.e. closure ). Is it still true that Foo will return w/o waiting ??

    Thanks

  • @nativecpp: It doesn't matter where the variable was defined... Foo will not wait for the Task to complete.

Page 1 of 2 (22 items) 12