为什么异步无效异常会使应用程序崩溃,但异步任务却被吞没

时间:2018-11-08 20:22:27

标签: c# .net asynchronous async-await

我了解到async Task的异常可以被捕获:

try { await task; }
catch { }

async void之所以不能,是因为无法等待。

但是为什么不等待异步 Task (就像异步 void 一样)却吞下Exception,而 > void 导致应用程序崩溃?

呼叫者ex();

已致电

async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }

3 个答案:

答案 0 :(得分:13)

TL; DR

这是因为不应该使用async voidasync 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的{​​{1}}方法,这将是一个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(例如,我们在WPF应用程序的UI线程上),则会在该上下文中发布该异常。
  • 否则,该异常将在SynchronizationContext线程中排队。

在两种情况下,都不会在ThreadPool调用周围设置的try-catch块捕获异常(除非您有一个特殊的ex()可以执行此操作) ,请参见例如Stephen Cleary的AsyncContext)。

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

答案 1 :(得分:1)

因为,您的方法不是异步执行的。

执行将同步运行,直到它“满足” await关键字为止。

因此,在void的情况下,应用程序将引发异常,因为异常发生在当前执行上下文中。

Task甚至同步引发异常的情况下,它也会包装在Task中并返回给调用方。

如果您将在函数中使用void,则await也应获得预期的行为。

async void Ex()
{
    await Task.Delay(1000);
    throw new Exception();
}

答案 2 :(得分:1)

请阅读底部的重要说明。

async void方法将使应用程序崩溃,因为C#编译器没有将Task对象推入异常。从功能上讲,返回async的方法上的Task关键字只是繁重的语法糖,它告诉编译器使用各种可用的方法根据Task对象来重写您的方法从对象上来看,以及诸如Task.FromResultTask.FromExceptionTask.FromCancelled之类的实用程序,有时甚至是Task.Run,或从编译器的角度来看的等效项。这意味着代码如下:

async Task Except()
{
    throw new Exception { };
}

大约变成了

Task Except()
{
    return Task.FromException(new Exception { });
}

,因此,当您调用Task的{​​{1}}-返回async方法时,该程序不会崩溃,因为实际上没有引发异常。而是以“例外”状态创建throw对象,并将其返回给调用方。如前所述,装饰了Task的方法没有要返回的async void对象,因此编译器不会尝试使用Task对象来重写该方法,而是相反,它只会尝试获取等待的呼叫的值。

更多上下文

Task的返回方法实际上也可能导致异常,即使由于Task引起吞咽的原因而没有等待时,也是如此,因此,如果不存在,则方法中的异常也不会被吞下,如下所示。

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. } 返回throw方法中抛出的异常实际上是Task的原因,是因为async关键字应该被吞下通过设计await使异步环境中的调试更加容易。

重要提示

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