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

时间:2014-07-25 05:46:37

标签: c# asynchronous async-await

我在异步方法中抛出异常时会遇到一些我无法理解的行为。

以下代码将在调用ThrowNow方法时立即抛出异常。如果我对该行进行注释并直接抛出异常,那么吞没异常并且不会在Unobserved事件处理程序中引发异常。

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();
}

更令人困惑的是,如果我从async方法中移除ThrowNow关键字,则会再次吞下该异常。

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

2 个答案:

答案 0 :(得分:6)

  

我认为异步方法同步运行,直到达到阻塞方法。

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

如果直接从异步 void 方法抛出异常,异步机制会发现您无法观察到该异常 - 它不会被抛回调用者,因为异步方法中抛出的异常只会通过任务传播。 (返回的任务会出现故障。)在直接抛出第一个阻塞await表达式之前抛出的异常会很奇怪,但之后的异常会以不同的方式处理。

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

如果您将void方法更改为返回Task,那么您只会遇到可以被观察到的异常,但不是(因为您'我没有在TestExAsync中使用返回值。

这有什么意义吗?如果你想要更多的澄清,请告诉我 - 它有点曲折(我不知道它有多少记录)。

编辑:我在C#5规范第10.15.2节中找到了文档:

  

如果async函数的返回类型为void,则评估与以上方式不同:由于没有返回任务,该函数会将当前线程的同步上下文的完成和异常通知。同步上下文的确切定义是依赖于实现的,但是表示当前线程正在运行的“where”。当评估void返回异步函数开始,成功完成或导致未捕获的异常被抛出时,将通知同步上下文。

答案 1 :(得分:0)

  

更令人困惑的是,如果我从ThrowNow方法中删除了异步关键字,则会再次吞下异常。

例外情况不会被“吞噬”。

除了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);
    }
}