Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
416 views
in Technique[技术] by (71.8m points)

c# - Why does Exception from async void crash the app but from async Task is swallowed

I understand that an async Task's Exceptions can be caught by:

try { await task; }
catch { }

while an async void's cannot because it cannot be awaited.

But why is it that when the async Task is not awaited (just like the async void one) the Exception is swallowed, while the void's one crashes the application?

Caller: ex();

Called:

async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

TL;DR

This is because async void shouldn't be used! async void is only there to make legacy code work (e.g. event handlers in WindowsForms and WPF).

Technical details

This is because of how the C# compiler generates code for the async methods.

You should know that behind async/await there's a state machine (IAsyncStateMachine implementation) generated by the compiler.

When you declare an async method, a state machine struct will be generated for it. For your ex() method, this state machine code will look like:

void IAsyncStateMachine.MoveNext()
{
    try
    {
        throw new Exception();
    }
    catch (Exception exception)
    {
        this.state = -2;
        this.builder.SetException(exception);
    }
}

Note that this.builder.SetException(exception); statement. For a Task-returning async method, this will be an AsyncTaskMethodBuilder object. For a void ex() method, it will be an AsyncVoidMethodBuilder.

The ex() method body will be replaced by the compiler with something like this:

private static Task ex()
{
    ExAsyncStateMachine exasm;
    exasm.builder = AsyncTaskMethodBuilder.Create();
    exasm.state = -1;
    exasm.builder.Start<ExAsyncStateMachine>(ref exasm);
    return exasm.builder.Task;
}

(and for the async void ex(), there will be no last return line)

The method builder's Start<T> method will call the MoveNext method of the state machine. The state machine's method catches the exception in its catch block. This exception should normally be observed on the Task object - the AsyncTaskMethodBuilder.SetException method stores that exception object in the Task instance. When we drop that Task instance (no await), we don't see the exception at all, but the exception itself isn't thrown anymore.

In the state machine for async void ex(), there's an AsyncVoidMethodBuilder instead. Its SetException method looks different: since there's no Task where to store the exception, it has to be thrown. It happens in a different way, however, not just a normal throw:

AsyncMethodBuilderCore.ThrowAsync(exception, synchronizationContext);

The logic inside that AsyncMethodBuilderCore.ThrowAsync helper decides:

  • If there's a SynchronizationContext (e.g. we're on a UI thread of a WPF app), the exception will be posted on that context.
  • Otherwise, the exception will be queued on a ThreadPool thread.

In both cases, the exception won't be caught by a try-catch block that might be set up around the ex() call (unless you have a special SynchronizationContext that can do this, see e.g. Stephen Cleary's AsyncContext).

The reason is simple: when we post a throw action or enqueue it, we then simply return from the ex() method and thus leave the try-catch block. Then, the posted/enqueued action is executed (either on the same or on a different thread).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...