【问题标题】:Timeout for asynchronous Task<T> with additional exception handling带有额外异常处理的异步 Task<T> 超时
【发布时间】:2020-02-28 18:32:47
【问题描述】:

在我的项目中,我从动态链接库中引用类型和接口。 使用这个特定库时,我要做的第一件事是创建EA.Repository 的实例,该实例在库中定义,用作进一步使用的入口点。

实例化EA.Repository repository = new EA.Repository() 在后台执行一些复杂的操作,我发现自己面临三种可能的结果:

  1. 实例化需要一些时间,但最终成功完成
  2. 抛出异常(立即或一段时间后)
  3. 实例化永远阻塞(在这种情况下,我想取消并通知用户)

我能够使用Task 提出一种异步方法:

public static void Connect()
{
    // Do the lengthy instantiation asynchronously
    Task<EA.Repository> task = Task.Run(() => { return new EA.Repository(); });

    bool isCompletedInTime;

    try
    {
        // Timeout after 5.0 seconds
        isCompletedInTime = task.Wait(5000);
    }
    catch (Exception)
    {
        // If the instantiation fails (in time), throw a custom exception
        throw new ConnectionException();
    }

    if (isCompletedInTime)
    {
        // If the instantiation finishes in time, store the object for later
        EapManager.Repository = task.Result;
    }
    else
    {       
        // If the instantiation did not finish in time, throw a custom exception
        throw new TimeoutException();
    }
}

(我知道,您可能已经在这里发现了很多问题。请耐心等待我......我们将不胜感激!)

到目前为止,这种方法有效 - 我可以模拟“异常”和“超时”两种情况,并获得所需的行为。

但是,我已经确定了另一种极端情况:假设实例化任务花费的时间足够长,以至于超时到期,然后引发异常。在这种情况下,我有时会收到一个AggregateException 说该任务尚未被观察到。

我正在努力寻找可行的解决方案。当超时到期时我无法真正取消任务,因为阻塞实例化显然阻止了我使用CancellationToken 方法。

我唯一能想到的就是在抛出我的自定义 TimeoutException 之前开始异步观察任务(即启动另一个任务):

Task observerTask = Task.Run(() => {
    try { task.Wait(); }
    catch (Exception) { }
});

throw new TimeoutException();

当然,如果实例化真的永远阻塞,我已经有第一个任务永远无法完成。有了观察者任务,现在我什至有两个!

我对整个方法很不安全,所以欢迎任何建议!

非常感谢您!

【问题讨论】:

  • 您的目标是 .NET Framework 4 或更低版本吗? .NET Framework 4.5 及更高版本会忽略未观察到的任务异常,除非您通过使用 ThrowUnobservedTaskExceptions enabled="true" 中的 ThrowUnobservedTaskExceptions enabled="true" 设置明确告诉它不要这样做。(documentation)
  • 好点!我不知道这种行为是可配置的。我的应用程序的目标是 .NET 框架 4.6.1。但是,即使我的App.config 没有指定&lt;ThrowUnobservedTaskExceptions&gt; 标记,我也得到了上述异常,即它应该默认为false。无论如何,也许让未观察到的任务抛出异常会更好,因为它指出我没有正确处理任务,对吧?
  • 奇怪。尝试处理静态TaskScheduler.UnobservedTaskException 事件,并调用事件argsSetObserved() 方法。不,必须处理这些异常是不行的,因为当垃圾收集器回收未引用的Task 时,它们是非确定性地引发的。
  • 哦,没错...我实际上已经处理了TaskScheduler.UnobservedTaskException 事件并调用了SetObserved()。但之后我将异常写入日志文件,通知用户并尝试正常关闭应用程序。那么,我在这一点上的想法是错误的吗?我是否应该将未观察到的任务异常视为“不再感兴趣,因为我故意放弃了任务”并默默地丢弃异常?
  • 很高兴了解争论的双方(顺便说一句,非常感谢您为我指出视频中的确切时间戳!)我认为,至少目前,我将坚持我的“未观察到的任务异常是可疑的,尽量不要放弃任务”的方法,因为它指出我的任务中出现了我没有解释的问题。尽管如此,我现在理解了这类异常的非确定性行为,并且它们被抛出的时间点并没有真正说明太多......

标签: c# asynchronous timeout interop


【解决方案1】:

我不确定我是否完全理解你想要达到的目标,但是如果你做这样的事情会怎样 -

public static void Connect()
{
    Task<EA.Repository> _realWork = Task.Run(() => { return new EA.Repository(); });
    Task _timeoutTask = Task.Delay(5000);
    Task.WaitAny(new Task[]{_realWork, timeoutTask});
    if (_timeoutTask.Completed)
    {
        // timed out
    }
    else
    {
        // all good, access _realWork.Result
    }
}

或者你甚至可以缩短一点 -

public static void Connect()
{
    Task<EA.Repository> _realWork = Task.Run(() => { return new EA.Repository(); });
    var completedTaskIndex = Task.WaitAny(new Task[]{_realWork}, 5000);
    if (completedTaskIndex == -1)
    {
        // timed out
    }
    else
    {
        // all good, access _realWork.Result
    }
}

您也可以随时 call Task.Run with a CancellationToken 超时,但这会引发异常 - 上述解决方案可让您控制行为而不会引发异常(即使您始终可以 try/catch

【讨论】:

  • 这个版本实际上看起来相当整洁!我可以指出,在您的第一个建议中,您的意思可能类似于Task _timeoutTask = Task.Run(() =&gt; { Task.Delay(5000).Wait(); });,因为否则_timeoutTask 似乎总是设置其IsCompleted 属性,因为它从未执行过。我已尝试调整您的解决方案,以便捕获 _realWork 任务中的预期异常,但我最终得到了我提到的相同边缘情况。我想我的问题基本上归结为旧的“如何取消不可取消的任务”问题:-/
【解决方案2】:

这是一个扩展方法,您可以使用它来显式观察在未观察到时可能失败的任务:

public static Task<T> AsObserved<T>(this Task<T> task)
{
    task.ContinueWith(t => t.Exception);
    return task;
}

使用示例:

var task = Task.Run(() => new EA.Repository()).AsObserved();

【讨论】:

  • 这是个好主意!使用ContinueWith(),我现在可以明确地承认我让任务自行处理,但我知道它稍后可能会完成或失败(TaskContinuationOptions 甚至给了我一些灵活性)。如果我不小心放弃了一项任务并且它失败了,TaskScheduler.UnobservedTaskException 事件处理程序仍然会捕获它。不敢相信我想出立即开始观察者任务,但不是在真正需要时才开始“捕捉任务”......再次感谢!
  • 我的荣幸,没人先生!是的,设置标志TaskContinuationOptions.OnlyOnFaulted 应该会让这个小软件的意图更加明显。 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-26
  • 1970-01-01
  • 1970-01-01
  • 2019-08-29
相关资源
最近更新 更多