5

Could anyone please explain this, perhaps I'm missing something obvious.

These 2 cases seem to be identical in behavior, and yet they are not.

Case 1:

  • Start a Task with an async Action, that does some work for some time:

    var t = Task.Run(async () => { await Task.Delay(2000); });
    
  • A second task waits for the first one:

    var waitingTask = Task.Run(() => { t.Wait(); });
    
  • Wait for the second task:

    waitingTask.Wait();
    

Case 2:

  • Build a Task using the Task constructor, passing the same async Action:

    var t = new Task(async () => { await Task.Delay(2000); });
    
  • Start another task to Wait for the first one (just like in the first case):

    var waitingTask = Task.Run(() => { t.Wait(); });
    
  • Start the first task:

    t.Start();
    
  • Wait for the second task:

    waitingTask.Wait();
    

The first case behaves as expected: the waiting task ends after the first one does, after 2 seconds.

The second case is weird: the waiting task ends very quickly, long before the first one does.

It's easy to see when printing messages from both tasks. A print at the end of the second task will show the difference.

I'm using VS 2015 Preview, which probably uses Roslyn to compile, if this matters.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
Fengari
  • 453
  • 3
  • 6
  • 2
    possible duplicate of [Waiting for async/await inside a task](http://stackoverflow.com/questions/24777253/waiting-for-async-await-inside-a-task) – Etienne Maheu Jan 21 '15 at 00:55

3 Answers3

8

The answer is here http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

In effect the issue is that with new Task, and that it can only take a Action, whilst you are trying to give a Func<Task>. Here are the first two lines again, but rewritten using the correct overloads.

Task t = Task.Run(async () => { await Task.Delay(2000); });

and

Task<Task> t = new Task<Task>(async () => { await Task.Delay(2000); });

The first creates a task as expected. The second creates a task, who's return type is a task that waits 2000ms. The first example works because of the following overloads...

 public Task Run(Func<Task>);
 public Task<T> Run(Func<T>);
 public Task<T> Run(Func<Task<T>>);
 etc...

These overloads are designed to call Task<Task<T>>.Unwrap() automatically for you.

The result is in your second example you are actually awaiting the start/enqueuing of the first task.

You can fix the second example by

var t = new Task<Task>(async () => { await Task.Delay(2000); }).Unwrap();
Aron
  • 15,464
  • 3
  • 31
  • 64
  • 1
    This is the same as the accepted answer from http://stackoverflow.com/questions/24777253/waiting-for-async-await-inside-a-task – Etienne Maheu Jan 21 '15 at 01:31
  • 1
    This post answers your question perfectly, though it misses an important detail. You need to have a Task to unwrap. It should read as `var t = new Task(async Task.Delay(2000)).Unwrap();` Without the generic parameter, you basically state that your lambda doesn't return anything so the awaited Task ends up getting called through fire-and-forget. Your question was why is there a difference between the two. The answer is because one is unwraped and not the other. How it should be done is not even part of the question I proposed as duplicate. – Etienne Maheu Jan 21 '15 at 01:40
3

It's discouraged to use a Task constructor with async-await, you should only be using Task.Run:

var t = Task.Run(async () => await Task.Delay(2000));

The Task constructor was built before the [Task-based Asynchronous Pattern (TAP)][1] and async-await. It will return a task that just starts the inner task and moves on without waiting for it to complete (i.e. fire and forget).

You could solve that by specifying your desired result as Task<Task> and use Unwrap to create a task that represents the entire operation including the inner task:

var t = new Task<Task>(async () => await Task.Delay(2000));   
t.Unwrap().Wait();

Using Task.Run is simpler and does all that for you, including Unwrap so you get back a Task in return that represents the entire async operation.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Sadly `Task.run` doesn't help if you're spinning up multiple long running tasks that need to call async actions, and you need to keep a record of those tasks somewhere. – Douglas Gaskell Jul 30 '17 at 00:55
  • @DouglasGaskell Why doesn't it help? `Task.Run` also returns a task you can keep track of. – i3arnon Jul 30 '17 at 09:49
2

Answer from Aron is not 100% correct. If you use a fix suggested, you will end up with:

Start may not be called on a promise-style task.

In my case, I solved a similar problem by holding references to both wrapped and unwrapped tasks. You use wrappedTask.Start() to start the task, but unwrappedTask.Wait() to await it (or to access anything related to task's execution, like its TaskStatus).

(Source)

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
  • When I tested Aron's proposed solution, the inner task was never started, so if you used `await` or `.Wait()` on the unwrapped task, it would wait forever. and then, like you said, if you try to `.Start()` the unwrapped task, it throws the exception you mentioned. – Austin Arnett Dec 30 '20 at 15:52