为什么异步的异常void会导致应用程序崩溃,但是异步会导致任务被吞噬

isp*_*iro 6 .net c# asynchronous async-await

我知道async Task可以通过以下方式获取例外情况:

try { await task; }
catch { }
Run Code Online (Sandbox Code Playgroud)

虽然async void不能,因为它无法等待.

但是为什么不等待async Task(就像异步void一样)Exception被吞下,而void的那个崩溃了应用程序?

来电者:ex();

:

async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }
Run Code Online (Sandbox Code Playgroud)

dym*_*oid 20

TL; DR

这是因为async void不应该使用!async void只有那里才能使遗留代码工作(例如WindowsForms和WPF中的事件处理程序).

技术细节

这是因为C#编译器如何为async方法生成代码.

您应该知道编译器生成的状态机(实现)后面async/ 后面.awaitIAsyncStateMachine

声明async方法时,struct将为其生成状态机.对于您的ex()方法,此状态机代码将如下所示:

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

请注意该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;
}
Run Code Online (Sandbox Code Playgroud)

(而且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);
Run Code Online (Sandbox Code Playgroud)

AsyncMethodBuilderCore.ThrowAsync帮助者内部的逻辑决定:

  • 如果有SynchronizationContext(例如我们在WPF应用程序的UI线程上),则会在该上下文上发布异常.
  • 否则,异常将在一个ThreadPool线程上排队.

在这两种情况下,异常都不会被try-catch可能围绕ex()调用设置的块捕获(除非您有一个特殊的SynchronizationContext可以执行此操作,请参阅Stephen Cleary的AsyncContext).

原因很简单:当我们发布一个throw动作或排队时,我们只需从该ex()方法返回,然后离开该try-catch块.然后,执行发布/排队的动作(在相同或不同的线程上).


Ale*_*nat 5

请阅读底部的重要说明。

async void方法会使应用程序崩溃,因为TaskC# 编译器没有对象可以将异常推送到其中。在功能层面上,async在关键词Task-returning方法就是重语法糖,它告诉编译器重写你的方法中的条款Task使用对象上提供的各种方法,对象,以及如公用事业Task.FromResultTask.FromExceptionTask.FromCancelled,或有时Task.Run,或从编译器的角度等价物。这意味着代码如下:

async Task Except()
{
    throw new Exception { };
}
Run Code Online (Sandbox Code Playgroud)

变成大约

Task Except()
{
    return Task.FromException(new Exception { });
}
Run Code Online (Sandbox Code Playgroud)

所以当你调用Task-returningasync方法时throw,程序不会崩溃,因为实际上没有抛出异常;相反,Task创建一个处于“例外”状态的对象并返回给调用者。如前所述,async void-decorated 方法没有Task要返回的对象,因此编译器不会尝试根据Task对象重写该方法,而只会尝试处理获取等待调用的值。

更多上下文

Task-returning 方法实际上也可以导致异常,即使没有被等待,因为async关键字是导致吞咽的原因,所以如果它不存在,则方法中的异常将不会被吞咽,如下所示。

Task Except() // Take note that there is no async modifier present.
{
    throw new Exception { }; // This will now throw no matter what.
    return Task.FromResult(0); // Task<T> derives from Task so this is an implicit cast.
}
Run Code Online (Sandbox Code Playgroud)

等待调用实际上会throwTask-returningasync方法中抛出异常的原因是因为该await关键字应该抛出被吞下的Exception设计s 以使在异步上下文中调试更容易。

重要的提示

这些“重写”实际上由编译器处理并由编译代码显示的方式可能与我暗示的方式不同,但在功能级别上大致相同。