1

I have run into a strange behaviour using async and await. If I try to await a manually created task T1, that itself is supposed to await another task T2, the task T1 has already run to completion even if task T2 is still awaiting.

To illustrate the problem I have written some code. When I run 'task1' from this example, the output is:

task1 ran to completion updating...

When I instead run 'task2', the output is always:

updating... task2 ran to completion

Does anybody have an explanation why the first await expression doesn't depend on the inner await?

private void OnLoaded(object sender, RoutedEventArgs e)
{
    var task1 = new Task(async () => await UpdateAfterDelay());
    task1.Start();
    await task1;
    Console.WriteLine("task1 ran to completion");

    var task2 = Task.Run(async () => await UpdateAfterDelay());
    await task2;
    Console.WriteLine("task2 ran to completion");
}

private async Task UpdateAfterDelay()
{
    await Task.Delay(2000);
    Console.WriteLine("updating...");
}
Johnny
  • 8,939
  • 2
  • 28
  • 33
Dennis Kassel
  • 2,726
  • 4
  • 19
  • 30
  • 6
    You're running into strange behaviour because you are doing *very strange things*. You already have an asynchronous method that returns a task; why are you going through all this rigamarole of creating an new task with an asynchronous lambda? If you want two tasks with two awaits, just do `await Update(); await Update();` and you're done. Can you explain why you are trying to build this excessively complicated and confusing workflow when it can be expressed simply? – Eric Lippert Oct 17 '19 at 19:30
  • 1
    The `Task` constructor doesn't understand async delegates. This means that it doesn't recognize `Func` as something special. It thinks that `Task` is just a normal return value. So you end up getting a return value of `Task`. In these cases you must await for the completion of the outer and the inner `Task` separately, which is achieved with `await await`. So to make your code behave as expected you must make two small changes: 1) use the generic `Task` constructor (`new Task`) instead of the non generic one. 2) await two times the resulting task. – Theodor Zoulias Oct 17 '19 at 21:00
  • @Theodor Zoulias That would work. Thanks. But the inferred type is not Task because of using the await operator. So I am still confused about why this doesn't work. If I declare the the expression without using await, the compiler informs me, that the call is not awaited so that execution will continue. That doesn't make sense since using await leads to the same behaviour. – Dennis Kassel Oct 18 '19 at 08:50
  • I am trying to encapsulate a whole bunch of asynchronous expressions within a single method. Since using async / await requires you to make your whole API async, I can't avoid making these methods async (to summarise, I am trying to do a computing-intensive operation on a different thread and have to update my UI after that). But regardless of my code I am now interested in why these two approaches behave differently. Since both are taking an Action callback, I wouldn't expect that. And omitting the await operator results in a compiler warning if i write "new Task(() => UpdateAfterDelay())". – Dennis Kassel Oct 18 '19 at 08:59
  • You are currently using the non-generic `Task` constructor, that accepts an argument of type `Action`. Not `Func`. So the return value of your `UpdateAfterDelay` method is discarded. This becomes a problem because the return value of this method is a `Task`, and you have no way to await this `Task` because a reference to it is not returned. So your code becomes parallel, with the ignored `Task` and the code after `task1.Start()` running concurrently. You may think that `await task1;` awaits the aforementioned task, but it isn't. It awaits the outer task that creates the ignored inner task. – Theodor Zoulias Oct 18 '19 at 09:15
  • The `Task.Run` method has meny overloads. One of them accepts an argument of type `Action`. You are not calling that. You are calling this overload: [`Run(Func)`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=netframework-4.8#System_Threading_Tasks_Task_Run_System_Func_System_Threading_Tasks_Task__). *Queues the specified work to run on the thread pool and returns a proxy for the task returned by function.* Note the word "proxy". This means that you are handled a convenient combination of the outer and inner task, that hides this complexity from you. – Theodor Zoulias Oct 18 '19 at 09:21

3 Answers3

5

Daniel has the right idea. If you look at the constructors for Task, they all only accept Action objects, which are essentially void methods. This means that your anonymous method is interpreted as async void, which is "fire and forget" (start it, and don't wait for it).

This becomes more clear if you don't use anonymous methods:

var task1 = new Task(UpdateAfterDelayTask); //this does not compile

var task2 = new Task(UpdateAfterDelayVoid); //this does

private async Task UpdateAfterDelayTask()
{
    await Task.Delay(2000);
}

private async void UpdateAfterDelayVoid()
{
    await Task.Delay(2000);
}

The task1 assignment complains that the method you are giving it has the wrong return type.

Task.Run, however, has an overload that accepts a Func<Task> (a method that returns a Task). So the compiler inspects your anonymous method, sees that it returns a Task, and picks the Func<Task> overload. That overload returns a new Task that depends on the Task returned by your anonymous method.

All that said, maybe there's a reason you're using new Task or Task.Run that you haven't shared, but as-is, you don't actually need to. You can do this:

var task1 = UpdateAfterDelay();
// do something else
await task1;

If you're not "doing something else", then you don't need the task1 variable at all. Just await UpdateAfterDelay().

Also noteworthy is this article that explains why you almost never need to use new Task(), and this later article (written after Task.Run came out) that explains why you almost always want to use Task.Run() (if you even need to).

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Just a note: The article [“Task.Factory.StartNew” vs “new Task(…).Start”](https://devblogs.microsoft.com/pfxteam/task-factory-startnew-vs-new-task-start) doesn't actually suggest to never use `new Task()`. Nor the article [Task.Run vs Task.Factory.StartNew](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew) suggests to always use `Task.Run()`. Both articles present possible uses for the more specialized methods. – Theodor Zoulias Oct 17 '19 at 20:23
  • 1
    @TheodorZoulias True... I clarified that sentence. – Gabriel Luci Oct 17 '19 at 20:30
  • Stephen Cleary has a stronger opinion than Stephen Toub [about task constructors](https://blog.stephencleary.com/2014/05/a-tour-of-task-part-1-constructors.html) though: *Do not use Task or Task constructors.* :-) – Theodor Zoulias Oct 17 '19 at 21:10
  • That seems to be so. Even if Task.Run also accepts an Action object just like the contructor does, it may handle it differently. But it is worth noting that in my example there is no type Func inferred since I declare the Action callback as an async method. So if I would declare "new Task(async()=> await UpdateAfterDelayTask())" the compiler woud complain nothing and one could expect the task to be awaitable. – Dennis Kassel Oct 18 '19 at 09:27
  • @Dennis Kassel an anonymous async delegate is interpreted either as `Func` or as `Func>`. It depends on what follows after =>. In your case you point to an async void method, so the async delegate is interpretted as `Func`. When the `new Task` constructor is presented with a `Func` argument, is allowed to use it like it was an `Action` by discarding the return value. Compare it with this line of code: `Math.Abs(0);`. This is allowed. It is totally useless since the `Math.Abs` method has no side-effects, but you are allowed to do it. – Theodor Zoulias Oct 18 '19 at 09:59
  • I have found further information on this topic. The behaviour is caused by the constructor not being async-aware. Only the Run-method is able to await asynchronous expressions. This is also important for developers who need to create their own TaskScheduler. https://blog.stephencleary.com/2015/03/a-tour-of-task-part-9-delegate-tasks.html – Dennis Kassel Oct 18 '19 at 14:08
  • That's the point I was trying to make in my answer. The `Task` constructor only accepts an `Action`, which is the same as a `void` method. You cannot be async-aware of a `void` method, because you need a `Task` object to be async-aware. – Gabriel Luci Oct 18 '19 at 14:15
  • @DennisKassel the generic `Task` constructor **can** await asynchronous expressions, by `await await Task(async () =>...`. It's just too complicated to do it this way, and this is why the shortcut `Task.Run` was introduced together with the async/await functionality back in 2012 (C# 5). – Theodor Zoulias Oct 19 '19 at 12:11
2

What I assume is happening here is that your async lambda passed into the Task constructor is being implicitly converted into an Action, since the Task class does not contain a constructor accepting a Func<Task>, which is what you'd want. Therefore it's executing as if it were an Action - and immediately returns. Check the type used in the constructor in your IntelliSense to confirm this.

Daniel Crha
  • 675
  • 5
  • 13
-1

The other answers both have the right idea but to summarize and simplify:

using

var task1 = new Task(...)
task1.Start();

with an async lambda creates a void Task that runs in background and returns immediately (so await task1 doesn't actually need to wait)

but using

var task2 = Task.Run(...)

creates an awaitable Task that still runs in the background but will actually return a value that await can wait for.

LampToast
  • 542
  • 2
  • 9
  • Try this code: `var t = new Task(() => Thread.Sleep(1000)); t.Start(); t.Wait();` It'll definitely cause the calling thread to wait. – Theodor Zoulias Oct 17 '19 at 20:30
  • @TheodorZoulias ```.Wait()``` is different from ```await``` – LampToast Oct 17 '19 at 20:32
  • Replace `t.Wait();` with `await t;`. Same thing. – Theodor Zoulias Oct 17 '19 at 20:37
  • @Lamp: Critical to the original question is that the delegate passed to the `Task` constructor is instantiated from an `async` lambda. The delegate invocation returns immediately, since the first (only) think it does is `await`. Your post doesn't address this crucial aspect at all, and indeed what you wrote does not apply in any scenario where that aspect doesn't hold. I fail to see how this contributes anything useful, never mind something that would add to the existing answers. – Peter Duniho Oct 17 '19 at 20:48
  • added four words since you couldn’t figure out I was answering the original question. the top answer is good but covers the more general case which makes the answer more complex. As I stated, my answer was meant to simplify for this specific case. – LampToast Oct 17 '19 at 21:53
  • Try this, with async lambda. Still makes the calling thread to wait. `var t = new Task(async () => Thread.Sleep(1000)); t.Start(); await t;` – Theodor Zoulias Oct 17 '19 at 22:45