I'm trying to understand the difference between Factory.StartNew and Task.Run. I saw the equivalence in various places like here.
I think I have to use Factory.StartNew() in my case as I want to plug in my own TaskScheduler.
So to sum up, it seems that:
Task.Run(action) Is strictly equivalent to:
Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); But, I ran a few tests with a simple SerialQueue grabbed from Microsoft's samples for Parallel Programming with the .NET Framework.
Here is the simple code:
/// <summary>Represents a queue of tasks to be started and executed serially.</summary> public class SerialTaskQueue { /// <summary>The ordered queue of tasks to be executed. Also serves as a lock protecting all shared state.</summary> private Queue<object> _tasks = new Queue<object>(); /// <summary>The task currently executing, or null if there is none.</summary> private Task _taskInFlight; /// <summary>Enqueues the task to be processed serially and in order.</summary> /// <param name="taskGenerator">The function that generates a non-started task.</param> public void Enqueue(Func<Task> taskGenerator) { EnqueueInternal(taskGenerator); } /// <summary>Enqueues the task to be processed serially and in order.</summary> /// <param name="taskOrFunction">The task or functino that generates a task.</param> /// <remarks>The task must not be started and must only be started by this instance.</remarks> private void EnqueueInternal(object taskOrFunction) { // Validate the task if (taskOrFunction == null) throw new ArgumentNullException("task"); lock (_tasks) { // If there is currently no task in flight, we'll start this one if (_taskInFlight == null) StartTask_CallUnderLock(taskOrFunction); // Otherwise, just queue the task to be started later else _tasks.Enqueue(taskOrFunction); } } /// <summary>Starts the provided task (or function that returns a task).</summary> /// <param name="nextItem">The next task or function that returns a task.</param> private void StartTask_CallUnderLock(object nextItem) { Task next = nextItem as Task; if (next == null) next = ((Func<Task>)nextItem)(); if (next.Status == TaskStatus.Created) next.Start(); _taskInFlight = next; next.ContinueWith(OnTaskCompletion); } /// <summary>Called when a Task completes to potentially start the next in the queue.</summary> /// <param name="ignored">The task that completed.</param> private void OnTaskCompletion(Task ignored) { lock (_tasks) { // The task completed, so nothing is currently in flight. // If there are any tasks in the queue, start the next one. _taskInFlight = null; if (_tasks.Count > 0) StartTask_CallUnderLock(_tasks.Dequeue()); } } } And now here is my code of some simulated composed task (including await/continuation).
public static async Task SimulateTaskSequence(int taskId) { Console.WriteLine("Task{0} - Start working 1sec (ManagedThreadId={1} IsThreadPoolThread={2})", taskId, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); Thread.Sleep(200); Console.WriteLine("Task{0} - Zzz 1st 1sec (ManagedThreadId={1} IsThreadPoolThread={2})", taskId, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); await Task.Delay(200); Console.WriteLine("Task{0} - Done (ManagedThreadId={1} IsThreadPoolThread={2})", taskId, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); } Test1: using the queue with Task.Run():
static void Main(string[] args) { Console.WriteLine($"Starting test program (ManagedThreadId={Thread.CurrentThread.ManagedThreadId} IsThreadPoolThread={Thread.CurrentThread.IsThreadPoolThread})"); SerialTaskQueue co_pQueue = new SerialTaskQueue(); for (int i = 0; i < 2; i++) { var local = i; co_pQueue.Enqueue(() => Task.Run(() => { return SimulateTaskSequence(local); })); } } And the result is correct, the queue is processed in the expected order (achieve Task0 before switching to Task1).
Starting test program (ManagedThreadId=1 IsThreadPoolThread=False)
Task0 - Start working 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 - Zzz 1st 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 - Done (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 - Start working 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 - Zzz 1st 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 - Done (ManagedThreadId=8 IsThreadPoolThread=True)
Test 2: just using Factory.StartNew with its perfect equivalence:
static void Main(string[] args) { Console.WriteLine($"Starting test program (ManagedThreadId={Thread.CurrentThread.ManagedThreadId} IsThreadPoolThread={Thread.CurrentThread.IsThreadPoolThread})"); SerialTaskQueue co_pQueue = new SerialTaskQueue(); for (int i = 0; i < 2; i++) { var local = i; co_pQueue.Enqueue(() => Task.Factory.StartNew(() => { return SimulateTaskSequence(local); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); } } But this time I get the following output:
Starting test program (ManagedThreadId=1 IsThreadPoolThread=False)
Task0 - Start working 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 - Zzz 1st 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 - Start working 1sec (ManagedThreadId=5 IsThreadPoolThread=True) WHAT?
Task1 - Zzz 1st 1sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 - Done (ManagedThreadId=9 IsThreadPoolThread=True)
Task1 - Done (ManagedThreadId=5 IsThreadPoolThread=True)
I don't get the difference. Why is the behavior different? I thought it was equivalent?! (remember, the step after is plugging in my own scheduler)