TL;DR
这是因为不应该使用async void! async void 仅用于使遗留代码工作(例如 WindowsForms 和 WPF 中的事件处理程序)。
技术细节
这是因为 C# 编译器如何为 async 方法生成代码。
你应该知道,在async/await后面有一个由编译器生成的状态机(IAsyncStateMachine实现)。
当您声明一个async 方法时,将为它生成一个状态机struct。对于您的 ex() 方法,此状态机代码如下所示:
void IAsyncStateMachine.MoveNext()
{
try
{
throw new Exception();
}
catch (Exception exception)
{
this.state = -2;
this.builder.SetException(exception);
}
}
注意this.builder.SetException(exception); 声明。对于Task-返回async 方法,这将是一个AsyncTaskMethodBuilder 对象。对于void ex() 方法,它将是AsyncVoidMethodBuilder。
ex() 方法体将被编译器替换为如下内容:
private static Task ex()
{
ExAsyncStateMachine exasm;
exasm.builder = AsyncTaskMethodBuilder.Create();
exasm.state = -1;
exasm.builder.Start<ExAsyncStateMachine>(ref exasm);
return exasm.builder.Task;
}
(对于async void ex(),不会有最后一行return)
方法构建器的Start<T> 方法将调用状态机的MoveNext 方法。状态机的方法在其catch 块中捕获异常。通常应该在 Task 对象上观察到此异常 - AsyncTaskMethodBuilder.SetException 方法将该异常对象存储在 Task 实例中。当我们删除 Task 实例(没有 await)时,我们根本看不到异常,但异常本身不再抛出。
在async void ex() 的状态机中,有一个AsyncVoidMethodBuilder。它的SetException 方法看起来不同:因为没有Task 存储异常的位置,所以必须抛出它。然而,它以不同的方式发生,而不仅仅是普通的throw:
AsyncMethodBuilderCore.ThrowAsync(exception, synchronizationContext);
AsyncMethodBuilderCore.ThrowAsync helper 内部的逻辑决定:
- 如果存在
SynchronizationContext(例如,我们在 WPF 应用程序的 UI 线程上),则会在该上下文中发布异常。
- 否则,异常将在
ThreadPool 线程上排队。
在这两种情况下,可能围绕ex() 调用设置的try-catch 块不会捕获异常(除非您有一个特殊的SynchronizationContext 可以做到这一点,请参见例如斯蒂芬·克利里的@ 987654321@).
原因很简单:当我们发布 throw 操作或入队 它时,我们只需从ex() 方法返回,从而离开@987654365 @ 堵塞。然后,执行已发布/入队的操作(在同一线程或不同线程上)。