繁体   English   中英

等待超时后,使用Task.Wait记录两次C#/ Tasks错误

[英]C#/Tasks Error being logged twice with Task.Wait after wait timeout

等待超时后用Task.Wait抛出两次错误

该问题似乎源于我正在使用Task.Wait超时的事实。 问题是,要么在任务超时之后记录两次异常,要么在未记录超时之前抛出错误。 我添加了代码和测试,以更好地理解这种情况。

该测试背后的想法是,我们正在强制在引发异常(在3秒)之前发生超时(在2秒时)。 在这种情况下,例外情况会怎样? 结果如下。 从不报告“繁荣”异常。 它仍然是“任务”中未观察到的异常。

         [MassUpdateEngine.cs]
        // Package the collection of statements that need to be run as a Task.
        // The Task can then be given a cancellation token and a timeout.
        Task task = Task.Run(async () =>
        {
            try
            {
                Thread.Sleep(3000);
                throw new Exception("boom");

                // Checking whether the task was cancelled at each step in the task gives us finer grained control over when we should bail out.
                token.ThrowIfCancellationRequested();
                Guid id = SubmitPreview();
                results.JobId = id;

                token.ThrowIfCancellationRequested();
                bool previewStatus = await GetPreviewStatus(id, token);
                Logger.Log("Preview status: " + previewStatus);

                token.ThrowIfCancellationRequested();
                ExecuteUpdate(id);

                token.ThrowIfCancellationRequested();
                bool updateStatus = await GetUpdateStatus(id, token);
                Logger.Log("Update status: " + updateStatus);

                token.ThrowIfCancellationRequested();
                string value = GetUpdateResults(id);
                results.NewValue = value;
            }
            // It appears that awaited methods will throw exceptions on when cancelled.
            catch (OperationCanceledException)
            {
                Logger.Log("***An operation was cancelled.***");
            }
        }, token);

        task.ContinueWith(antecedent =>
        {
            //Logger.Log(antecedent.Exception.ToString());
            throw new CustomException();
        }, TaskContinuationOptions.OnlyOnFaulted);



         [Program.cs]
        try
        {
            MassUpdateEngine engine = new MassUpdateEngine();

            // This call simulates calling the MassUpdate.Execute method that will handle preview + update all in one "synchronous" call.
            //Results results = engine.Execute();

            // This call simulates calling the MassUpdate.Execute method that will handle preview + update all in one "synchronous" call along with a timeout value.
            // Note: PreviewProcessor and UpdateProcessor both sleep for 3 seconds each.  The timeout needs to be > 6 seconds for the call to complete successfully.
            int timeout = 2000;
            Results results = engine.Execute(timeout);

            Logger.Log("Results: " + results.NewValue);
        }
        catch (TimeoutException ex)
        {
            Logger.Log("***Timeout occurred.***");
        }
        catch (AggregateException ex)
        {
            Logger.Log("***Aggregate exception occurred.***\n" + ex.ToString());
        }
        catch (CustomException ex)
        {
            Logger.Log("A custom exception was caught and handled.\n" + ex.ToString());
        }

由于从未观察到异常,因此未正确记录该异常,将无法正常工作。

此信息导致以下规则:

  1. 不要从.ContinueWith中抛出。 从这里引发的异常不会编组回调用线程。 这些异常仍然是未观察到的异常,并被有效地吞噬了。
  2. 使用带有超时的等待时,可能会或可能不会将Task中的异常封送回调用线程。 如果异常发生在Wait调用超时之前,则该异常将编组回调用线程。 如果在等待调用超时后发生异常,则该异常将保留为任务的未观察到异常。

规则2非常难看。 在这种情况下,我们如何可靠地记录异常? 我们可以使用.ContinueWith / OnlyOnFaulted记录异常(请参见下文)。

        task.ContinueWith(antecedent =>
        {
            Logger.Log(antecedent.Exception.ToString());
            //throw new CustomException();
        }, TaskContinuationOptions.OnlyOnFaulted);

但是,如果异常在Wait调用超时之前发生,则该异常将被整理回调用线程,并由全局未处理的异常处理程序处理(并记录),然后将其传递给.ContinueWith任务(并记录),导致针对同一异常的两个错误日志条目。

这里一定有我想念的东西。 任何帮助,将不胜感激。

不要从.ContinueWith中抛出。 从这里引发的异常不会编组回调用线程。 这些异常仍然是未观察到的异常,并被有效地吞噬了。

未观察到异常的原因是因为未观察到任务(不是因为它是使用ContinueWith创建的)。 ContinueWith返回的任务只是被忽略。

我会说更合适的建议是根本不使用ContinueWith (有关更多详细信息,请参见我的博客)。 一旦更改为使用await ,答案就会更加清晰:

public static async Task LogExceptions(Func<Task> func)
{
  try
  {
    await func();
  }
  catch (Exception ex)
  {
    Logger.Log(ex.ToString());
  }
}

public static async Task<T> LogExceptions<T>(Func<Task<T>> func)
{
  try
  {
    return await func();
  }
  catch (Exception ex)
  {
    Logger.Log(ex.ToString());
  }
}

async / await模式更清楚地说明了这里的实际情况:代码正在创建包装器任务 ,该任务必须用作原始任务的替代品

Task task = LogExceptions(() => Task.Run(() => ...

现在,包装器任务将始终观察其内部任务的任何异常。

使用带有超时的等待时,可能会或可能不会将Task中的异常封送回调用线程。 如果异常发生在Wait调用超时之前,则该异常将编组回调用线程。 如果在等待调用超时后发生异常,则该异常将保留为任务的未观察到异常。

我会从异常的位置而不是“编组”或“调用线程”的角度来考虑这一点。 那只会使问题困惑。 当任务发生故障时,该任务将被放置例外(并且该任务已完成)。 因此,如果在等待完成之前将异常放置在任务上,则任务完成会满足等待,并引发异常。 如果等待在任务完成之前超时,则等待不再等待,因此当然看不到异常,并且该异常可能无法观察到。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM