【问题标题】:How can I propagate an exception thrown from an asynchronous function to an awaiting async void function?如何将异步函数引发的异常传播到等待的异步 void 函数?
【发布时间】:2015-12-16 04:55:59
【问题描述】:

我有一长串调用,最终调用另一个程序集中的异步函数。我希望这个函数被同步执行,它可能抛出一个异常,我想把它传播到调用链上。

下面的 sn-p 最小限度地再现了这个场景:

static Task<int> Exc()
{
    throw new ArgumentException("Exc");
    return Task.FromResult(1);
}

static async void DoWork()
{
    await Exc();
}

static void Main(string[] args)
{
    try
    {
        DoWork();
    }
    catch (Exception e)
    {
        Console.WriteLine("Caught {0}", e.Message);
    }
}

此代码将导致崩溃,因为从Exc 抛出的异常不会传播回Main

我有什么办法可以让Exc 中的catch 块处理Main 中抛出的异常,而无需更改调用链中的每个函数以使用asyncawait,并返回一个Task?

在我的实际代码中,这个异步函数是从一个非常深的函数调用链中调用的,所有这些函数都应该同步执行。当它们永远不会被异步使用(也不能安全地使用)时让它们全部异步似乎是一个糟糕的主意,如果可能的话,我想避免它。

【问题讨论】:

  • 您可以安装自定义同步上下文,因为错误将被发布到它。我认为 Nito Async 有一个。
  • @usr 这不会让异常被“由 Main 中的 catch 块处理”
  • @i3arnon 同步上下文可以等到异步 void 方法完成。然后将出现异常。 async void 方法甚至不使用同步上下文中的“未完成操作”计数器吗?没有把握。更新:是的,他们有。
  • @usr 它将存在,但它不能真正传播到main,因为它以同步方法运行。要掌握异常并对其进行处理,您只需在任务上注册一个延续。
  • 您愿意进行哪些修改?你愿意修改 Main 吗?修改 DoWork? void DoWork() { Exec().Wait(); } 会为你工作吗?

标签: c# asynchronous async-await


【解决方案1】:

此代码将导致崩溃,因为从 Exc 抛出的异常不会传播回 DoWork。

一点也不。来自Exc 的异常被传递给DoWork,如果您在DoWork 中有try/catch,则可以在那里捕获。

您看到崩溃的原因是因为DoWork 正在传播该异常,而DoWorkasync void 方法。这可以通过将DoWork 改为async Task 方法来轻松避免(注意async void 应该用于事件处理程序,DoWork 显然不是)。正如我在关于最佳实践的 MSDN 文章中所描述的,请努力 avoid async void

在我的实际代码中,这个异步函数是从一个非常深的函数调用链中调用的,所有这些函数都应该同步执行。当它们永远不会被异步使用(也不能安全地使用)时让它们全部异步似乎是一个糟糕的主意,如果可能的话,我想避免它。

操作要么是异步的,要么不是。由于您调用的低级 API 是异步的,因此您的代码最好异步使用它。尝试将异步代码包装在同步代码中非常容易出错,而且是一个糟糕的主意。虽然,有时,这是必要的。

所以,最干净的解决方案是让异步方法具有异步签名。是的,这意味着使用“async all the way”,正如我在 MSDN 关于异步最佳实践的文章中所描述的那样。但是,如果您更喜欢同步而不是异步,那么您可以选择variety of hacks that I describe in my recent "Brownfield async" article 之一。

【讨论】:

    【解决方案2】:

    根据MSDN,没有。

    您主要使用 void 返回类型来定义需要该返回类型的事件处理程序。返回 void 的异步方法的调用者不能等待它,也不能捕获该方法抛出的异常。

    【讨论】:

    • 在我的实际代码中,我要处理的catch块和异步函数之间有16个函数调用。至此,async/await已经基本成为我代码的毒瘤了。
    • 您能否在调用链的底部同步运行async 函数并重新抛出async 操作引发的任何异常?
    • 除非我遗漏了什么,否则我可以从非异步函数同步运行它的唯一方法是执行Exc().Wait(),这可能会导致死锁。
    【解决方案3】:

    如果您坚持使整个堆栈异步,我将提供斯蒂芬斯回答的反驳点:

    void DoWork() { Exec().Wait(); } 对你有用吗?

    控制台应用程序中没有异步死锁。如果这不是控制台应用程序,您可以使用what Ivan linked to,或使用ConfigureAwait(false),或使用不会死锁的Task.Run(...).Wait(),因为任务主体没有同步上下文。

    请注意,执行上述任何操作都会丢失异步 IO 的任何潜在收益。

    【讨论】:

    • 如果整个线程池在 Wait() 调用上被阻塞,这两种方法中的任何一种都不会死锁吗?
    • 是的,任何不完全异步的方法都有这个问题。没有解决方法,那就是异步 IO 的 point。不过,这个问题非常罕见。您是否计划同时运行 100 次这样的计算?
    • 是的;异步函数实际上需要 5-10 秒才能完成,并且我的调用链的入口点来自服务主机(由开发人员在内部使用)。我有一个非常特殊的情况,我需要等待,但通常消费者不会愿意。
    • 您知道异步根本不会使 IO 运行得更快吗?以防万一我会将我的标准材料链接到您了解何时使用异步以及何时不使用:stackoverflow.com/a/25087273/122718 为什么 EF 6 教程使用异步调用? stackoverflow.com/a/12796711/122718我们应该切换到默认使用异步 I/O 吗?
    猜你喜欢
    • 2022-07-06
    • 2017-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-29
    • 1970-01-01
    相关资源
    最近更新 更多