8

I'm running the following code (C#7.1 Console App), and I can't really understand why the difference in behavior.

If I await a regular async method call, or a Task.Run - it works as expected (i.e. the app doesn't return immediately). But if I use Task.Factory.StartNew - it will return immediately without the code actually running.

Strangely enough - if I use StartNew but inside the method remove the await, it will not return immediately...

Problem: This returns immediately:

static async Task Main(string[] args)
{
    await Task.Factory.StartNew(DisplayCurrentInfo);
}

static async Task DisplayCurrentInfo()
{
    await WaitAndApologize();
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Thread.Sleep(3000);
}

i.e. - I won't get to see anything printed out to the console, and the console will already be shut down.

No problem: this doesn’t return immediately:

static async Task Main(string[] args)
{
    await DisplayCurrentInfo(); // or await Task.Run(DisplayCurrentInfo);
}

static async Task DisplayCurrentInfo()
{
    await WaitAndApologize();
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Thread.Sleep(3000);
}

Strange: this also doesn't return immediately:

static async Task Main(string[] args)
{
    await Task.Factory.StartNew(DisplayCurrentInfo); 
}

static async Task DisplayCurrentInfo()
{
    WaitAndApologize();
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Thread.Sleep(3000);
}

WaitAndApologize:

static async Task WaitAndApologize()
{
    // Task.Delay is a placeholder for actual work.  
    await Task.Delay(2000);
    // Task.Delay delays the following line by two seconds.  
    Console.WriteLine("\nSorry for the delay. . . .\n");
}
Maverick Meerkat
  • 5,737
  • 3
  • 47
  • 66
  • 3
    By "return immediately" you mean "immediately returns a task that hasn't completed yet", right? If so then that is expected and by design. If that wasn't what you meant, please clarify. Do you get a completed task in return? – Lasse V. Karlsen Jun 22 '18 at 07:11
  • 3
    And don't wrap a task inside a task, you need double awaits. You're spawning a task A using `Task.Run`, the purpose of this task is to create another task, B, and you're awaiting A, but not B. Unless you know how to do this correctly, don't use that call syntax, simply do what works. – Lasse V. Karlsen Jun 22 '18 at 07:11
  • 1
    A quick fix that will probably leave you with more questions if you didn't understand my second comment is to just use double awaits: `await await Task.Factory....`, but a *better* fix is to go with just `await DisplayCurrentInfo();`. – Lasse V. Karlsen Jun 22 '18 at 07:14
  • 1
    Once you're starting down the route of writing `async` code, any *manual* construction of `Task`s (via `Run` or `StartNew`) is probably questionable. You `async` methods already create `Task`s - why do you want even more of them? – Damien_The_Unbeliever Jun 22 '18 at 07:15
  • @LasseVågsætherKarlsen interesting, so await await is legal syntax i followed what you were saying i think. Could you post what you said with more details and examples, i thought it was quite helpful in understanding what is happening.Thx, yes i know i didn't ask the question, but liked your explanation. – Seabizkit Jun 22 '18 at 07:18
  • 1
    If you wrap enough tasks inside each other you can write all the awaits you want, `await X` gives a new expression, if this expression is something that has an awaiter, you can await that as well, and so on ad infinitum. Taskception. – Lasse V. Karlsen Jun 22 '18 at 07:20
  • Where did you see that it's recommended to use `Task.Factory.StartNew` with `async-await`? – Paulo Morgado Jun 22 '18 at 09:27

3 Answers3

12

If you use Task.Factory.StartNew(MethodThatReturnsTask) you get back a Task<Task<T>> or Task<Task> depending on whether the method is returning a generic task or not.

The end result is that you have 2 tasks:

  1. Task.Factory.StartNew spawns a task that calls MethodThatReturnsTask, let's call this task "Task A"
  2. MethodThatReturnsTask in your case returns a Task, let's call this "Task B", this means that an overload of StartNew that handles this is used and the end result is that you get back a Task A that wraps Task B.

To "correctly" await these tasks needs 2 awaits, not 1. Your single await simply awaits Task A, which means that when it returns, Task B is still executing pending completion.

To naively answer your question, use 2 awaits:

await await Task.Factory.StartNew(DisplayCurrentInfo);

However, it is questionable why you need to spawn a task just to kick off another async method. Instead you're much better off using the second syntax, where you simply await the method:

await DisplayCurrentInfo();

Opinion follows: In general, once you've started writing async code, using Task.Factory.StartNew or any of its sibling methods should be reserved for when you need to spawn a thread (or something similar) to call something that isn't async in parallel with something else. If you're not requiring this particular pattern, it's best to not use it.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • 1
    Instead of await await, I would do a Unwrap of the first task. But I agree with your overall conclusion, don't user StartNew if it's not needed. – Ronald Jun 22 '18 at 07:33
  • 1
    Ah... I understand now. Though you didn't answer the "strange" case, but VibeeshanRC explained it below. Thanks! – Maverick Meerkat Jun 22 '18 at 09:51
2

When you call Task.Factory.StartNew(DisplayCurrentInfo); you are using the signature:

public Task<TResult> StartNew<TResult>(Func<TResult> function)

Now since you are passing a method with the signature Task DisplayCurrentInfo() then the TResult type is Task.

In other words you are returning a Task<Task> from Task.Factory.StartNew(DisplayCurrentInfo) and you are then awaiting the outer task which returns Task - and you're not awaiting that. Hence it completes immediately.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
2
await Task.Factory.StartNew(DisplayCurrentInfo);

The first example does return imediatly, because you are creating a new Task and awaiting it, which will call another task but that is not waited. Think about a psudo code like below inorder to keep awaited.

await Task.Factory.StartNew( await DisplayCurrentInfo); //Imaginary code to understand

In the third example WaitAndApologize is not awaited, but its getting blocked by thread sleep (Full Thread is blocked).

With only the help of code we can't say that the Task factory has created a new thread or just running on the existing thread. If its running in the same thread, the whole thread is getting blocked, so you are getting a feeling that the code is awaiting at await Task.Factory.StartNew(DisplayCurrentInfo); , but atually its not happening.

If its running on a differnet thread, you will not get the same result as above.

Vibeeshan Mahadeva
  • 7,147
  • 8
  • 52
  • 102
  • 1
    If you were to rewrite the second piece of code to actually compile you would need to write an async lambda expression or anonymous method, in which case you now have 3 tasks and you're still left with the double await on the outside. – Lasse V. Karlsen Jun 22 '18 at 07:23
  • plus one and Thanks for the explanation about the strange case. I get it now. – Maverick Meerkat Jun 22 '18 at 09:51