【问题标题】:Why does Exception from async void crash the app but from async Task is swallowed为什么来自异步无效的异常会使应用程序崩溃但来自异步任务的异常被吞没
【发布时间】:2019-04-12 10:10:51
【问题描述】:

我了解async Task 的异常可以通过以下方式捕获:

try { await task; }
catch { }

async void 不能,因为它不能被等待。

但是为什么当 async Task 没有被等待时(就像 async void 一样)Exception 被吞噬了,而 void 的应用程序崩溃了?

来电者ex();

调用

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

【问题讨论】:

标签: c# .net asynchronous async-await


【解决方案1】:

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

执行将同步运行,直到“遇到”await 关键字。

所以在void的情况下应用程序会抛出异常,因为异常发生在当前执行上下文中。

如果Task即使是同步抛出的异常也会被包裹在Task中并返回给调用者。

如果您将在函数中使用await,您也应该使用void 获得所需的行为。

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

【讨论】:

  • in case of void application will throw an exception, because exception occurs in the current execution context - 那么为什么包裹在 try-catch 块中(在调用者中)没有被捕获?
  • await Task.Delay(1000); - 不。应用程序仍然崩溃。
  • 当 void 的应用程序崩溃时 - void 应该
【解决方案2】:

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-返回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&lt;T&gt; 方法将调用状态机的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 @ 堵塞。然后,执行已发布/入队的操作(在同一线程或不同线程上)。

【讨论】:

    【解决方案3】:

    请阅读底部的重要说明。

    async void 方法将使应用程序崩溃,因为 C# 编译器没有 Task 对象可以将异常推送到其中。在功能层面上,Task-returning 方法上的 async 关键字只是繁重的语法糖,它告诉编译器根据 Task 对象使用对象上可用的各种方法以及实用程序,例如 Task.FromResultTask.FromExceptionTask.FromCancelled,或者有时是 Task.Run,或者从编译器的角度来看的等价物。这意味着像这样的代码:

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

    变成大约

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

    所以当你调用Task-returning async 方法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.
    }
    

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

    重要提示

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

    【讨论】:

    • 异常回答亚历克斯。
    猜你喜欢
    • 1970-01-01
    • 2018-07-19
    • 1970-01-01
    • 2016-05-10
    • 2012-09-27
    • 1970-01-01
    • 2018-09-08
    • 1970-01-01
    相关资源
    最近更新 更多