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

2017/01/24

no comments

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

This post will focus on the key difference between Task.Run and Task.Factory.StartNew.

For you can drill down into more details on this excellent post by Stephen Toub.

In general you have to know that Task.Run is more restricted version of Task.Factory.StartNew.

Task.Run sets default which fit for most common cases.

Task.Factory.StartNew is there for advance scenarios.

 

in general Task.Run is equivalent to:

Code Snippet
  1. Task.Factory.StartNew(someAction,
  2.                 cancellationToken,
  3.                 TaskCreationOptions.DenyChildAttach,
  4.                 TaskScheduler.Default);

 

Why should you care about it and how can it be used right?

I will start with the last default parameter, TaskScheduler.Default, which is used by Task.Run.

The default scheduler for Task.Factory.StartNew is actually TaskSheduler.Current.

TaskScheduler.Default will schedule the over the Thread Pool (by default).

When using Task.Factory.StartNew on operation that already running under different scheduler

you may be surprise by it behavior.

 

Take a look on the following code snippet:

Code Snippet
  1. public class StockProvider
  2. {
  3.     // will raise on non UI thread
  4.     public event Action<Stock> Changes;
  5. }
  6.  
  7. public class ViewModel
  8. {
  9.     private readonly TaskScheduler _uiScheduler =
  10.         TaskScheduler.FromCurrentSynchronizationContext();
  11.  
  12.     // must be updated from the UI thread
  13.     public ObservableCollection<Stock> Data { get; } =
  14.                         new ObservableCollection<Stock>();
  15.     public ViewModel(StockProvider stockProvider)
  16.     {
  17.         stockProvider.Changes += OnChanges;
  18.     }
  19.  
  20.     private void OnChanges(Stock stock)
  21.     {
  22.         // Synchronized to the UI thread
  23.         Task.Factory.StartNew(() => OnChangesUISafe(stock),
  24.             CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
  25.     }
  26.     private void OnChangesUISafe(Stock stock)
  27.     {
  28.         Data.Add(stock);
  29.         Task.Factory.StartNew(() => UpdateDatabase(stock));
  30.     }
  31.  
  32.     private void UpdateDatabase(Stock stock)
  33.     {
  34.         // do database update
  35.     }
  36. }

At lien 13 it has resource which must be update from the UI thread.

At line 17 it register for event which raise on thread that is not the UI thread.

At line 23:  the code schedule the execution of the event handling on the UI thread.

.NET component which need message pump mechanism (like the UI in WPF) is using .NET component

called SynchronizationContext, the scheduler at line 9 is using SynchronizationContext.Current for scheduling the work.

the method OnChangesUISafe will be scheduled on the UI thread (if the creation of the class will happen on the UI thread).

this way line 28 is safe.

 

The question is on which thread will the UpdateDatabase be scheduled?

At line 29 we use Task.Factory.StartNew to offload the work from the UI thread (in order to avoid freezing),

but is it really schedule the method on other thread rather the UI thread?

If so, on which thread will it schedule the work?

 

As I mention before the default for Task.Factory.StartNew is TaskScheduler.Current which mean in this case

that the task will be schedule over the SynchronizationContext i.e. the UI thread.

This code will actually cause UI freeze.

 

Task.Run come to help you avoid of this kind of mistakes.

Replacing line 17 with:

Code Snippet
  1. private void OnChangesUISafe(Stock stock)
  2. {
  3.     Data.Add(stock);
  4.     Task.Run(() => UpdateDatabase(stock));
  5. }

Will schedule the UpdateDatabase over a ThreadPool thread.

 

Is it always good to use Task.Run instead of Task.Factory.StartNew?

The answer is obviously not.

You should use it in most cases but sometime you want to go beyond its limitations.

 

A big issue on parallel programming is how to schedule IO operations?

the catch is that IO operation are relatively slow and don’t use the CPU (it may use

the network adapter or the hard disk controller but for most time the CPU is idle).

The problem start when people are using Thread from the thread pool in order to

preform synchronous IO operation in asynchronous way.

For Example:

Code Snippet
  1. Task.Run(() => UpdateDatabase(stock));

Why is It so bad?

It’s bad because the thread pool is optimize to do massive CPU bound operations and

when stilling one of its thread without using it for computation, it can become really slow

(I may write more of it on some other post).

 

What are our options?

The best option for executing IO operations in parallel is to await on method which

is using IO completion Port (for example HttpClient is using it when executing method like GetAsync, PostAsync, etc.).

 

But what if the provider don’t have a method that is using this technique?

When we schedule IO often we may consider using custom scheduler which will offload

the work to alternative (custom) pool which is using normal threads rather the thread pool.

When you have to schedule it for a few times you can simply

use TaskCreationOptions.LongRunning over the default Task Scheduler, it is kind of a hint for

the scheduler which make it schedule over new normal thread rather thread from the thread pool.

in either case this cannot be done by using Task.Run and you will have to choose Task.Factory.StartNew.

 

The following code snippet show simplified version of custom pool scheduling:

Code Snippet
  1. public class IOScheduler : TaskScheduler
  2. {
  3.     private readonly BlockingCollection<Task> _taskQueue = new BlockingCollection<Task>();
  4.  
  5.     public IOScheduler(int degreeOfParallelism)
  6.     {
  7.         for (int i = 0; i < degreeOfParallelism; i++)
  8.         {
  9.             var t = new Thread(Work, 10 *1024 /* 10K stack*/);
  10.             t.Name = $"IO {i}";
  11.             t.IsBackground = true;
  12.             t.Start();
  13.         }
  14.     }
  15.  
  16.     private void Work()
  17.     {
  18.         foreach (var task in _taskQueue.GetConsumingEnumerable())
  19.         {
  20.             base.TryExecuteTask(task);
  21.         }
  22.     }
  23.  
  24.     protected override void QueueTask(Task task)
  25.     {
  26.         _taskQueue.Add(task);
  27.     }
  28.  
  29.     protected override IEnumerable<Task> GetScheduledTasks()
  30.     {
  31.         return _taskQueue;
  32.     }
  33.  
  34.     protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
  35.     {
  36.         return false;
  37.     }

Using the Task Scheduler:

Code Snippet
  1. var scheduler = new IOScheduler(100);
  2. Task.Factory.StartNew(() => { /* non IOCP work */},
  3.     CancellationToken.None, TaskCreationOptions.None, scheduler);

Using long running hint:

Code Snippet
  1. Task.Factory.StartNew(() => { /* non IOCP work */},
  2.     TaskCreationOptions.LongRunning);

 

continue to part 2

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*