1

I'm well aware (thanks to Stephen Toub) that constructing a task with new Task(...) is generally not recommended and would normally prefer to use Task.Run, but what is the difference between the three approaches below when passing an async lambda as the task to run? I came across something similar to this in production code, so the code below is a highly contrived and simple example.

When passing an async lambda as a task to Task.Factory.StartNew and to new Task(...) (followed by Task.Start), even though we wait for the returned task the lambda does not finish. However, it does when using Task.Run - what's the difference here?

(and Stephen Toub states that Task.Run is exactly equivalent to

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

See https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/

Here is my code:

using System;
using System.Threading.Tasks;
using System.Threading;

namespace TaskDelay
{
    class Program
    {
        static readonly long t0 = DateTime.Now.Ticks;
        private static void Main()
        {
            Console.WriteLine($"{Time} Starting t1");
            var t1 = new Task(async () => await F1(5000, "Task 1"));
            t1.Start();
            t1.Wait();

            Console.WriteLine($"{Time} Starting t2");

            var t2 = Task.Factory.StartNew(async () => await F1(5000, "Task 2"), 
                                  CancellationToken.None, 
                                  TaskCreationOptions.DenyChildAttach, 
                                  TaskScheduler.Default);
            t2.Wait();

            Console.WriteLine($"{Time} Starting t3");
            var t3 = Task.Run(async () => await F1(2000, "Task 3"));
            t3.Wait();

            Console.WriteLine($"{Time} State of {nameof(t1)} is {t1.Status}");
            Console.WriteLine($"{Time} State of {nameof(t2)} is {t2.Status}");
            Console.WriteLine($"{Time} State of {nameof(t3)} is {t3.Status}");
        }


        private static async Task F1(int delay, string taskName)
        {
            await Console.Out.WriteLineAsync($"{Time} Started to run F1 for {taskName}");
            await Task.Delay(delay);
            await Console.Out.WriteLineAsync($"{Time} Finished running F1 for {taskName}");
        }
        private static string Time => $"{(int)((DateTime.Now.Ticks - t0) / 10_000),5} ms:";
    }
}

And the output is

enter image description here

Notice we never see "Finished running F1 for Task 1" or "Finished running F1 for Task 2".

Adrian S
  • 514
  • 7
  • 16
  • Does this answer your question? [Regarding usage of Task.Start() , Task.Run() and Task.Factory.StartNew()](https://stackoverflow.com/questions/29693362/regarding-usage-of-task-start-task-run-and-task-factory-startnew) – shingo Nov 29 '22 at 10:35
  • 1
    @shingo - no it doesn't. That question is asking why there are several methods to do apparently the same thing. My question is pointing out differences between at least two of the methods and asking why this is. – Adrian S Nov 29 '22 at 10:42
  • *"`Task.Factory.StartNew(SomeTask`"* -- I would suggest to rename the `SomeTask` to `SomeAsyncMethod` or `SomeAsyncDelegate`. A `Task` is not a method! – Theodor Zoulias Nov 29 '22 at 10:45
  • Yes it does, you just don't fully comprehend it. And you should also read the links in the answer, [link this one](https://devblogs.microsoft.com/pfxteam/task-factory-startnew-vs-new-task-start/). – JHBonarius Nov 29 '22 at 10:58
  • @JHBonarius the [linked question](https://stackoverflow.com/questions/29693362/regarding-usage-of-task-start-task-run-and-task-factory-startnew) is not a duplicate of this question. The linked question deals with synchronous delegates. This question deals with asynchronous delegates. – Theodor Zoulias Nov 29 '22 at 11:02
  • 1
    @TheodorZoulias but the accepted answer has the same link you post in your answer below. So the answer is there – JHBonarius Nov 29 '22 at 11:03
  • @JHBonarius just because the accepted answer in the other question contains a hint that is useful for answering this question, a hint that is borderline off-topic for the other question, does not make the two questions duplicates IMHO. If you want check out [this](https://meta.stackoverflow.com/questions/417476/question-close-reasons-definitions-and-guidance/417477#417477) meta guidance. Notice the "apples-to-apples" reference. – Theodor Zoulias Nov 29 '22 at 11:19
  • 1
    `Stephen Toub states that Task.Run is exactly equivalent to` - he states that those are exactly equivalent _when passing an `Action`_. But your code doesn't pass an `Action` (it's passing a `Func`), so they're _not_ equivalent in your case. – Stephen Cleary Nov 29 '22 at 15:26

2 Answers2

3
var t1 = new Task(async () => 
var t2 = Task.Factory.StartNew(async () => 

If you check the type of the t1 and t2 in the Visual Studio, you'll see that they are both Task<Task>. This is a Task whose Result is another Task. The outer Task represent the launching of the async operation, and the inner Task represents the completion of the async operation. When the outer Task completes, it means that the delegate that creates the inner Task has just completed, and so the inner Task has just been created. The creation of the inner Task happens on a ThreadPool thread (by default), so it happens asynchronously in regard to the current thread.

So you have two tasks to wait:

t1.Wait();
t1.Result.Wait();

Getting the t1.Result is blocking. It has the same effect with the t1.Wait(). So the first line t1.Wait(); is not really needed. The second line t1.Result.Wait(); waits both tasks by itself.

It is possible to do a single Wait, if you combine the two tasks to one with the Unwrap method:

t1.Unwrap().Wait();

The Task.Run method differs from the other two, because it is equipped with overloads that accept async delegates. These overloads are doing the Unwrap automatically, and so they return a Task, not a Task<Task>. For more details, check out this article: Task.Run vs Task.Factory.StartNew by Stephen Toub.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

To avoid inner/outer task problem explained by Theodor and to ensure you await the right task (rather than its wrapper task), you can:

  1. start the work directly, if you want to start the task immediately and just wait for it later:
    var t1 = F1(5000, "Task 1"); // no 'await', starts the work
    
    ... other work
    
    await t1; 
    
    (but be aware that the non-async work in F1 will be executed before we return from F1, and this doesn't work with fake async methods)
  2. use an async lambda, if you don't want to start the task immediately:
    Func<Task<int>> f = async () => await WorkAsync(); // no work is started
    
    var result = await f(); // *Starts* and waits
    
tymtam
  • 31,798
  • 8
  • 86
  • 126