Thursday, February 12, 2015

Task.Run() vs. Task.Factory.StartNew()

Last night, I gave a talk on the BackgroundWorker component (one of my favorite little tools). Part of that talk is to compare the the BackgroundWorker to using Task from the Task Parallel Library. As often happens, I got a question that I didn't know the answer to off the top of my head.
What's the difference between using "Task.Factory.StartNew()" and "Task.Run()"?
The demo code I showed used "Task.Factory.StartNew()" to create a Task and start it at the same time. I knew "Task.Run()" was a bit different but didn't know how since I generally use the Factory (it was a recommendation I picked up a while back). It turns out that the difference is how much control we can take over our newly created Task.

Creating a New Task
I've been writing quite a bit about Task and await recently. But so far, this has all been from the standpoint of consuming someone else's asynchronous methods. I have plans for articles that talk about creating our own asynchronous methods, so this is jumping a little bit ahead.

There are several different ways that we can create a new Task. First, we can simply use the constructor on the Task object. The downside of this is that it does not actually *start* the task. We have to call a separate method to do that. This may actually be an upside if we want to create a Task for future use.

But often we want to create *and* start a task at the same time. To do this, we have 2 options: "Task.Run()" and "Task.Factory.StartNew()". Both of these will create a Task, start it, and then return the Task object to us. This lets us assign it to a variable to keep track of the task, check for exceptions, or add continuations.

But one of these gives us a bit more control. Let's take a closer look.

Options with Task.Run()
We'll start by looking at the overloads that we have available when we use the "Task.Run()" method. Here is one of the most basic overloads:


This takes delegate as a parameter, in this case Func<TResult> -- a method that takes no parameters and returns a value (if you want more information of Func, check out my video series on Delegates). For our example, we're assuming that we want to return an integer value.

The most complex version of this method takes 2 parameters:


Here we can see that in addition to the delegate, we have another parameter for a cancellation token. And these are really the only options.

If we look at the other overloads, we see that there are a variety of different delegate types (some that return values, some that return Tasks, some that return void), but they are all very similar. Here's the list from the documentation:


Our only real choice for adding more information is to include a cancellation token.

Options with Task.Factory.StartNew()
If we look at the overloads for "Task.Factory.StartNew()", we'll see a number of other options that let us take more control over our Task.

Here's the basic version:


Just like "Task.Run()", this particular overload takes a delegate as a parameter. And whether we use "Task.Run(Func<int> function)" or "Task.Factory.StartNew(Func<int> function)", there is absolutely no difference in functionality. A Task would be created, it would be started (meaning the delegate would be invoked), and we would get a Task back as a return value that we can assign to a variable.

[Update: As Dan points out in the comments, there are differences in the defaults that are used for TaskCreationOptions and the TaskScheduler (mentioned below) depending on whether we use Task.Run() or Task.Factory.StartNew(). There are also some subtleties when it comes to "unwrapping" tasks. These differences become important when we create more complex Tasks. More details from Stephen Toub: Task.Run vs Task.Factory.StartNew.]

But "Task.Factory.StartNew()" has many more options. Here's the most complex overload of the method:


Lots of stuff to look at here.

object state
First, notice the 2nd parameter is an "object state". This allows us to pass an object that we can use as a parameter for our delegate. Notice that the 1st parameter is "Func<object,int>". This means that we have a delegate that takes an "object" as a parameter and returns an "int" value. The "object" parameter of the delegate comes from the "object state" parameter that we pass in.

CancellationToken
The next parameter is a CancellationToken. This is not much different from what we have in the "Task.Run()" options, and as you can imagine this allows us to pass in a token that is used for cancellation.

For a little more information on cancellation tokens, check out Task and await: Basic Cancellation.

TaskCreationOptions
The next parameter is where things start to get interesting: TaskCreationOptions. This is an enum that has the following values:


We won't go into the details here, but you can see just from the names that we can provide more information for our Task and how we want it to behave. We'll look at the specifics in a later article. For now, you can check out the MSDN article for descriptions of each option: TaskCreationOptions Enumeration.

TaskScheduler
The last parameter that we have is a TaskScheduler. Again, we won't go into the details of all of the options here, but you can take a look at the MSDN article on the TaskScheduler Class for an example.

One of the most common uses for this parameter is to use the current synchronization context. We actually took a look at this back when in the first article Task and Await: Consuming Asynchronous Methods.

In that article, we create a continuation -- a method that we want to run when our task is complete. But we initially ran into a problem because we also want to update UI controls. By default, the continuation was not running on the UI thread, but when we added a TaskScheduler as a parameter, we were able to get things to work:

From Task.ContinueWith in prior article

When we ask for a TaskScheduler "FromCurrentSynchronizationContext", it will use the synchronization context from where we called this method. Since the "ContinueWith" method is being called from the UI thread, it uses the synchronization context from that same thread. This means that we can access the UI controls from within our continuation without running into any problems.

So, just like with our continuation, we could use the UI context when we create a new Task if we need to interact with UI controls. Now, I would *not* recommend interacting with UI controls in a Task that we're trying to get off of the UI thread. There are other ways of getting information back to our UI to use (such as through progress reporting). We'll take a look at this in a future article as well.

Other Options
There are a total of 16 overloads for "Task.Factory.StartNew()" that combine these parameters in different ways. Here are the overloads from Help:


So, lots of options.

Wrap Up
So whether we use "Task.Run()" or "Task.Factory.StartNew()" really depends upon how much control we need to take over our Task. Personally, I've created a habit of always using "Task.Factory.StartNew()". But it looks like I'll need to reconsider this by looking at the defaults for each method and the other recommendations that are out there.

In future articles, we'll take a closer look at creating our own Tasks and when we might need to use these various options.

In the meantime, don't be afraid to ask questions. And don't be afraid to say "I don't know" in answer to a question. I do that all the time -- it's impossible to know everything, but we can always learn something new.

Happy Coding!

4 comments:

  1. While it doesn't come up often, it's good to be aware that Task.Run and Task.Factory.StartNew specify different defaults when you use the less explicit overloads.

    Most notably, they set different default task schedulers.

    Task.Run(action) equates to:
    Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

    Task.Factory.StartNew(action) equates to:
    Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current)

    Stephen Toub did a nice write up a few years ago about the differences:
    http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

    ReplyDelete
    Replies
    1. Thanks for the additional info, Dan. I didn't realize that the defaults for the TaskCreationOptions and TaskScheduler were different. I'll be sure to drop a note in the article. I'm a bit curious about the "DenyChildAttach" as the default for Task.Run. That seems like a strange limitation. It looks like I'll need to dig a little deeper.

      Delete
    2. It looks like the TaskScheduler might not be different (in many instances). In looking through the documentation, TaskScheduler.Default is the default scheduler for the .NET Framework. TaskScheduler.Current is the scheduler associated with the currently executing task. However, if TaskScheduler.Current is *not* called from within a task, then it is TaskScheduler.Default.

      So it looks like if we're creating a top-level Task (like the example that spawned the question), then TaskScheduler.Default and TaskScheduler.Current are equivalent.

      Tasks are fun, huh?

      Delete