从异步中捕获未处理的异常

25
当一个被等待执行的异步方法抛出异常时,该异常会被存储起来并且延迟抛出。在WinForms或WPF应用程序中,它使用 SynchronizationContext.Current 来推迟抛出异常。然而,在控制台应用程序等应用程序中,它会在线程池上抛出异常并使应用程序崩溃。
如何防止从异步方法抛出的异常导致应用程序崩溃?
编辑:
显然,我所描述的问题是因为我有 void async 方法。请查看评论。

2
如果等待异步方法,则异常会在等待它的代码中抛出。仅当方法返回void时,未处理的异常才会以这种方式运行。这是尽可能避免使用async void方法的原因。 - svick
1
async void 方法会产生确定性行为;如果调用返回 Task 的函数时发生异常并且不等待该任务,那么在未来某个半随机时间点上,当垃圾回收器运行时,将引发 UnobservedTaskException 事件,如果什么都不做,则程序会静默继续,好像一切正常。问题不在于 async void 方法,它们只是暴露了真正的问题。如果您从 async 方法中调用返回 Task 的函数,则很有可能您正在做一些错误的事情。 - user743382
顺便说一下,只有在一些特殊的情况下才可以放弃异常处理(不是恶意),但这种情况并不常见。 - user743382
@hvd:事件处理程序必须是void类型。为什么UnobservedTaskException在这里不是问题? - Pieter van Ginkel
1
@hvd: 好的,不是"随机"。:) 不过我对于async void的必要性持有异议。如果你需要一个async事件处理程序(通常在UI应用程序中),那么你会使用它,但其他情况下则不需要。ASP.NET WebAPI和MVC将大多数顶级入口点提供为async Task。对于op的情况(控制台应用程序),我仍然更喜欢async Task而不是async void - Stephen Cleary
显示剩余4条评论
2个回答

22
如何避免异步方法抛出的异常导致应用程序崩溃?
请遵循以下最佳实践:
1. 所有异步方法应该返回Task或Task<T>,除非它们必须返回void(例如,事件处理程序)。
2. 在某个时刻,您应该await所有从异步方法返回的Task。唯一不这样做的原因是如果您不再关心操作的结果(例如,在取消后)。
3. 如果您需要捕获异步void事件处理程序中的异常,则在事件处理程序中捕获它-就像在同步代码中所做的那样。
您可能会发现我的async / await intro post有所帮助;我在那里涵盖了其他几个最佳实践。

1
жҲ‘жҳҺзҷҪжҲ‘йҒҮеҲ°зҡ„й—®йўҳжҳҜеӣ дёәжҲ‘жңүvoid asyncж–№жі•гҖӮдҪҶжҳҜпјҢиҝҷжҳҜеҗҰж„Ҹе‘ізқҖжҲ‘жҸҸиҝ°зҡ„й—®йўҳд№ҹдјҡеҸ‘з”ҹеңЁдәӢ件еӨ„зҗҶзЁӢеәҸдёӯпјҹ - Pieter van Ginkel
是的,再强调一下第三点,如果您有一个async void事件处理程序,您可能希望在该事件处理程序内部捕获异常。同步事件处理程序将具有完全相同的问题-如果异常在线程池线程上运行时逃逸,它将使应用程序崩溃。 - Stephen Cleary
除了异常被路由到调用异步方法时捕获的“同步上下文”之外,其他都一样。在WinForms/WPF应用程序中,大多数情况下,这将是正确的同步上下文,因为事件很可能是从UI线程启动的。它看起来与事件不是“异步”时发生的情况并没有太大的区别。 - Pieter van Ginkel
1
SynchronizationContext路由是为了模拟同步事件处理程序的行为。来自同步事件处理程序和async void事件处理程序的异常最终都会进入上下文中。无论上下文是UI、ASP.NET还是线程池,这一点都是正确的。 - Stephen Cleary
谢谢!线程同步可能会解决问题,但这是正确/预期的方式。 - Rushui Guan

13

当启动 async 方法时,它会捕获当前的同步上下文。解决这个问题的方法是创建自己的同步上下文,它能捕获异常。

关键在于同步上下文将回调发布到线程池,但是需要使用 try/catch 将其包装:

public class AsyncSynchronizationContext : SynchronizationContext
{
    public override void Send(SendOrPostCallback d, object state)
    {
        try
        {
            d(state);
        }
        catch (Exception ex)
        {
            // Put your exception handling logic here.

            Console.WriteLine(ex.Message);
        }
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        try
        {
            d(state);
        }
        catch (Exception ex)
        {
            // Put your exception handling logic here.

            Console.WriteLine(ex.Message);
        }
    }
}
在上述的catch中,您可以放置异常处理逻辑。
接下来,在每个线程上(SynchronizationContext.Current[ThreadStatic]),如果要使用此机制执行async方法,则必须设置当前同步上下文:
SynchronizationContext.SetSynchronizationContext(new AsyncSynchronizationContext());

完整的Main示例:

class Program
{
    static void Main(string[] args)
    {
        SynchronizationContext.SetSynchronizationContext(new AsyncSynchronizationContext());

        ExecuteAsyncMethod();

        Console.ReadKey();
    }

    private static async void ExecuteAsyncMethod()
    {
        await AsyncMethod();
    }

    private static async Task AsyncMethod()
    {
        throw new Exception("Exception from async");
    }
}

5
抱歉,我没有看到您的SynchronizationContext在哪里将任何内容发布到线程池 - 就我所见,它所做的只是立即在调用线程上执行函数并返回。 - Rafael Munitić

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接