Task.Run vs. Task.Factory.StartNew (Part 2)
This post continue the previous post about the differences between Task.Run vs. Task.Factory,StartNew.
In the previous part I was speaking about the scheduling aspect, on this part I will spot the async await behavior.
Check the following code snippet and think what can go wrong:
What output will it produce?
Will it be something like the following output:
This is a reasonable guess but it isn’t the actual output.
The output of the code snippet it more like:
If you will try it with Task.Run:
You will get something like the first output.
Why is it behave differently? and What happens beneath the surface?
It’s all start with the overloads options of each API.
Task.Run offer Func<Task> and Func<Task<T>> while Task.Factory.StartNew only offer Action and Func<T>.
This it a big difference, because Task.Factory.StartNew don’t offer Func that return Task or Task<T>, therefore await cannot honor.
The following code snippet show the return value of each option.
This difference, is a lesson for API design. When delegate don’t return a Task, the invocation of it won’t be aware of the ending
of asynchronous operations. in matter of fact asynchronous operation will consider as fire and forget.
Let assume that you have the following API: Action<DIsposableResource>
in this case, if the caller will dispose after the call you want be able to produce async delegate without risking ObjectDisposedException.
See the following code snippet for better understanding:
When SafeExecution got called at line 14, the execution of ExecuteAsync is in great risk of being disposed before time, because
the API didn’t enable indication about the delegate completion time.
If you will get used to design delegate based API with async in mind you can gain better behavior for async execution .
The following code snippet show how this API should have been:
This snippet didn’t change a lot, but it’s much better in terms of enabling parallelism.
The change start at the API level. SafeInvocation (line 1) is getting getting Func<T, Task> rather Action<T>.
This make the all difference.
line 7 await the execution and the definition of ExecuteAsync has changed from void into async Task (line 17).
Back to our subject, Task.Run’s delegate honor async delegate by using overloads of Func which return Task or Task<T>.
In matter of fact I could do the same with Task.Factory.StartNew but it would require extra step which may be forgotten too many times.
Result of Task<Task> or Task<Task<T>> can be unwrap, i.e. extract the internal task from the result.
Following code snippet will produce the same output as Task.Run do:
The trick is the Unwrap at line 10, but this is something which can easily be forgotten.
As I mentioned on part 1 of the series Task.Run is here for ease our life and should be served for
the most common usage of simple parallelism.