Async方法立即抛出异常,但在删除Async关键字时被吞下

本文关键字:Async 关键字 删除 方法 抛出异常 | 更新日期: 2023-09-27 18:15:00

在异步方法中抛出异常时,我无法理解一些行为。

下面的代码将在调用ThrowNow方法时立即抛出异常。如果我注释掉那行并直接抛出异常,那么异常将被吞下,而不会在未观察到的事件处理程序中引发。

public static async void ThrowNow(Exception ex){
    throw ex;
}
public static async Task TestExAsync()
{
    ThrowNow(new System.Exception("Testing")); // Throws exception immediately
    //throw new System.Exception("Testing");   // Exception is swallowed, not raised in unobserved event
    await Task.Delay(1000);
}
void Main()
{  
    var task = TestExAsync();
}

更令人困惑的是,如果我从ThrowNow方法中删除async关键字,异常再次被吞噬。

我认为异步方法同步运行,直到到达阻塞方法。在这种情况下,删除async关键字似乎使它的行为异步。

Async方法立即抛出异常,但在删除Async关键字时被吞下

我认为异步方法在到达阻塞方法之前是同步运行的。

它们是这样做的,但是它们仍然意识到它们是在异步方法中执行的。

如果你直接从async void方法抛出异常,异步机制知道你没有办法观察到异常——它不会被扔回调用者,因为在异步方法中抛出的异常只通过任务传播。(返回的任务出现故障。)在第一个阻塞await表达式之前直接抛出异常是奇怪的,但是之后的异常处理方式不同。

据我所知,异步void方法抛出的异常直接传递给同步上下文,如果有的话。(延续被发布到同步上下文中,而同步上下文中只抛出异常。)在一个简单的控制台应用程序中,不是一个同步上下文,所以它被作为一个未报告的异常抛出。

如果你改变你的void方法返回Task,那么你只会有一个异常,可以被观察到,但不是(因为你没有使用TestExAsync的返回值)。

这有意义吗?如果你想要更多的澄清,请告诉我——这有点曲折(我不知道它的文档有多好)。

编辑:我发现了一个的文档,在c# 5规范章节10.15.2:

如果async函数的返回类型为void,则计算与上面的不同之处在于:因为不返回任何任务,所以该函数将完成和异常通知当前线程的同步上下文。同步上下文的确切定义依赖于实现,但它表示当前线程运行的"位置"。当返回空的异步函数的求值开始、成功完成或导致抛出未捕获的异常时,同步上下文将得到通知。

更令人困惑的是,如果我从ThrowNow方法中删除async关键字,异常将再次被吞噬。

异常不会被"吞下"。

除了Jon Skeet所说的,考虑以下代码,其中ThrowNow没有标记为async:

static void ThrowNow(Exception ex)
{
    throw ex;
}
static async Task TestExAsync()
{
    ThrowNow(new System.Exception("Testing"));
    await Task.Delay(1000);
}
static void Main()
{
    var task = TestExAsync();
    Console.WriteLine(task.Exception);
}

正如你所看到的,异常并没有被"吞下",它们只是在异步方法返回的任务中传达给你。

显然,这也意味着您不能try catch它们,除非您等待任务:

static void Main()
{
    AsyncMain();
}
static async void AsyncMain()
{
    var task = TestExAsync();
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}