【问题标题】:Async method throws exception instantly but is swallowed when async keyword is removedAsync 方法立即抛出异常,但在删除 async 关键字时被吞下
【发布时间】:2014-07-25 05:46:36
【问题描述】:

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

以下代码在调用 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();
}

有点混乱,如果我从 ThrowNow 方法中删除 async 关键字,异常会再次被吞没。

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

【问题讨论】:

    标签: c# asynchronous async-await


    【解决方案1】:

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

    他们知道,但他们仍然知道他们是在异步方法中执行的。

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

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

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

    这有意义吗?如果您需要更多说明,请告诉我 - 这有点曲折(而且我不知道它的文档记录如何)。

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

    如果 async 函数的返回类型为 void,则评估与上述不同之处在于:因为没有返回任务,所以该函数将完成和异常传达给当前线程的 同步上下文 .同步上下文的确切定义取决于实现,但它表示当前线程正在运行的“位置”。当返回 void 的异步函数的评估开始、成功完成或导致抛出未捕获的异常时,会通知同步上下文。

    【讨论】:

    • 是的,async 的一般建议是永远不要返回 void,除非您需要这样做以匹配方法签名(用于重载子类或实现接口)。
    • 据我所知,在控制台应用程序中使用 async-await 的文档并没有很好的记录。
    • “如果您直接从异步 void 方法抛出异常,异步机制会意识到您无法观察到该异常 - 它不会被抛回给调用者,” - 但是,在这种情况下,异步 void 方法会立即抛出异常,或者看起来如此。 (我应该在这里提到 LINQPad 的使用,所以我猜它是一个控制台应用程序)。难道是异步 void 方法中抛出的异常以如此快的方式进行,以至于它似乎立即抛出?顺便说一句,这不是一个未观察到的例外。
    • @VincePanuccio:不,问题不在于速度。该方法没有直接抛出异常——它告诉它的同步上下文有关异常,而后者又重新抛出它,因为它没有任何其他方式来指示它。
    【解决方案2】:

    有点混乱,如果我从 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);
        }
    }
    

    【讨论】:

    • @VincePanuccio 正如 Jon Skeet 所说,它们可以不被观察并发布到同步上下文中。但是您声称当 ThrowNow 未标记为 async 时,它们被“吞下”。我的回答告诉你,在那种情况下什么都没有被吞下,这似乎是你的问题所在。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-20
    • 2013-05-12
    • 2013-01-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多