What happens when you await a failed task

2019-05-02 20:20发布

问题:

I have a theoretical question to you. What happens if I await the Result of a Task inside another task? I want to know if my current system will work afterwards.

A task gets launched and does some stuff. At some point that task might need another one in order to process data which the current task is not able to process by itself. So I use await to ensure that the current task won't continue as long as he has not the result of the helper task. But what happens if the helper fails? Will the current task remain locked?

Can I avoid this deadlock somehow (without changing the system itself - task inside task)?

回答1:

A task gets launched and does some stuff. At some point that task might need another one in order to process data which the current task is not able to process by itself. So I use await to ensure that the current task won't continue as long as he has not the result of the helper task. But what happens if the helper fails? Will the current task remain locked?

The core idea behind async and await is that asynchronous code works just about the same as synchronous code.

So, if you have synchronous code like this:

void HelperMethod()
{
  throw new InvalidOperationException("test");
}

void DoStuff()
{
  HelperMethod();
}

then you would expect DoStuff to propagate the InvalidOperationException from the helper method. Similarly, that's what happens with asynchronous code:

async Task HelperMethodAsync()
{
    throw new InvalidOperationException("test");
}

async Task DoStuffAsync()
{
    await HelperMethodAsync();
}

That is, DoStuffAsync will also propagate the InvalidOperationException.

Now, it doesn't work exactly the same way, of course, since it must be asynchronous, but the general idea is that all your control flow such as try/catch, for loops, etc, all "just work" for asynchronous code very similarly to synchronous code.

What's actually going on is that when HelperMethod ends with an InvalidOperationException, the exception is caught and placed on the returned Task, and the task is completed. When the await in DoStuffAsync sees that the task has completed, it examines its exceptions and re-raises the first one (in this case, there is only one, the InvalidOperationException). And it re-raises it in a way that preserves the call stack on the exception. This in turn causes the Task returned from DoStuffAsync to be completed with that same exception.

So, under the covers async and await are doing a bit of work to ensure that you can just call other methods with await and use try/catch the same way as you would in synchronous code. But most of the time you don't have to be aware of that.



回答2:

What happens if I await the Result of a Task inside another task?

You can only await a Task.Result if it is an awaitable (meaning it has a GetAwaiter method). This is rarely the case. I assume you mean await on an inner Task.

But what happens if the helper fails? Will the current task remain locked?

First, im not sure what you mean by "locked". The task isn't locked while you await on the inner task. Control is yielded back to the calling method until that inner task completes. If that inner task fails and you fail to properly handle that exception, you parent task will fault as well. You need to make sure you handle exceptions gracefully:

var task = 
  Task.Run(async () =>
           {
                 try
                 {
                        await AsyncHelper.DoSomethingAsync();
                 }
                 catch (Exception e)
                 {
                       // Handle exception gracefully
                 }
           });

If you await on the parent Task, you'll notice the inner exception propogating from the unhandled inner Task.

Can I avoid this deadlock somehow?

I see no reason why your code should deadlock in this specific case. Not sure why this worries you.



回答3:

It's really easy to test. For example:

[TestMethod, ExpectedException(typeof(Exception))]
public async Task DoFaultedTaskThrowOnAwait()
{
  var task = Task.Factory.StartNew(() => { throw new Exception("Error 42"); });
  await task;
}

[TestMethod, ExpectedException(typeof(AggregateException))]
public void DoFaultedTaskThrowOnWait()
{
  var task = Task.Factory.StartNew(() => { throw new Exception("Error 42"); });
  task.Wait();
}

Both tests pass, notice that Wait throws an AggregateException, and await throws the Exception.