简而言之
这是因为不应该使用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
的内部逻辑如下:
- 如果存在
SynchronizationContext
(例如,在WPF应用程序的UI线程上),则异常将在该上下文中发布。
- 否则,异常将在
ThreadPool
线程上排队。
在这两种情况下,异常都不会被可能设置在ex()
调用周围的try-catch
块捕获(除非您有一个特殊的SynchronizationContext
可以执行此操作,例如参见Stephen Cleary的AsyncContext
)。
原因很简单:当我们发布或排队 throw
操作时,我们只需从ex()
方法返回,因此离开了try-catch
块。然后,发布/排队的操作将被执行(在同一线程上或在不同的线程上执行)。