简体   繁体   English

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

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

Error being thrown twice with Task.Wait after wait timeout 等待超时后用Task.Wait抛出两次错误

This problem seems to stem from the fact that I'm using Task.Wait for a timeout. 该问题似乎源于我正在使用Task.Wait超时的事实。 The issue is that either the exception thrown after the task timeout is logged twice or an error thrown before the timeout isn't logged. 问题是,要么在任务超时之后记录两次异常,要么在未记录超时之前抛出错误。 I've added the code and test I used to try to better understand the scenario. 我添加了代码和测试,以更好地理解这种情况。

The idea behind this test is that we are forcing the timeout to occur (at 2 seconds) BEFORE the exception is thrown (at 3 seconds). 该测试背后的想法是,我们正在强制在引发异常(在3秒)之前发生超时(在2秒时)。 What happens with the exception in this situation? 在这种情况下,例外情况会怎样? Below is the result. 结果如下。 The “boom” exception is never reported. 从不报告“繁荣”异常。 It remains as an unobserved exception in the Task. 它仍然是“任务”中未观察到的异常。

         [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());
        }

Because the exception isn't ever observed and therefore not appropriately logged this won't work. 由于从未观察到异常,因此未正确记录该异常,将无法正常工作。

This information leads to the following rules: 此信息导致以下规则:

  1. Don't throw from .ContinueWith. 不要从.ContinueWith中抛出。 Exceptions thrown from here aren't marshaled back to the calling thread. 从这里引发的异常不会编组回调用线程。 These exceptions remain as unobserved exceptions and are effectively eaten. 这些异常仍然是未观察到的异常,并被有效地吞噬了。
  2. An exception from a Task may or may not be marshalled back to the calling thread when using Wait with a timeout. 使用带有超时的等待时,可能会或可能不会将Task中的异常封送回调用线程。 If the exception occurs BEFORE the timeout for the Wait call, then the exception gets marshalled back to the calling thread. 如果异常发生在Wait调用超时之前,则该异常将编组回调用线程。 If the exception occurs AFTER the timeout for the Wait call, then the exception remains as an unobserved exception of the task. 如果在等待调用超时后发生异常,则该异常将保留为任务的未观察到异常。

Rule #2 is pretty ugly. 规则2非常难看。 How do we reliably log exceptions in this scenario? 在这种情况下,我们如何可靠地记录异常? We could use the .ContinueWith / OnlyOnFaulted to log the exception (see below). 我们可以使用.ContinueWith / OnlyOnFaulted记录异常(请参见下文)。

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

However, if the exception occurs BEFORE the timeout for the Wait call, then the exception will get marshalled back to the calling thread and handled by the global unhandled exception handler (and logged) AND will then be passed to the .ContinueWith task (and logged), resulting in two error log entries for the same exception. 但是,如果异常在Wait调用超时之前发生,则该异常将被整理回调用线程,并由全局未处理的异常处理程序处理(并记录),然后将其传递给.ContinueWith任务(并记录),导致针对同一异常的两个错误日志条目。

There must be something that I'm missing here. 这里一定有我想念的东西。 Any help would be appreciated. 任何帮助,将不胜感激。

Don't throw from .ContinueWith. 不要从.ContinueWith中抛出。 Exceptions thrown from here aren't marshaled back to the calling thread. 从这里引发的异常不会编组回调用线程。 These exceptions remain as unobserved exceptions and are effectively eaten. 这些异常仍然是未观察到的异常,并被有效地吞噬了。

The cause of the unobserved exception is because the task is not observed (not because it was created using ContinueWith ). 未观察到异常的原因是因为未观察到任务(不是因为它是使用ContinueWith创建的)。 The task returned from ContinueWith is just being ignored. ContinueWith返回的任务只是被忽略。

I'd say the more appropriate advice is to not use ContinueWith at all (see my blog for more details). 我会说更合适的建议是根本不使用ContinueWith (有关更多详细信息,请参见我的博客)。 Once you change to use await , the answer becomes clearer: 一旦更改为使用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());
  }
}

The async / await pattern more clearly illustrates what's really going on here: the code is creating a wrapper task that must be used as a replacement for the original task: async / await模式更清楚地说明了这里的实际情况:代码正在创建包装器任务 ,该任务必须用作原始任务的替代品

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

Now the wrapper task will always observe any exception of its inner task. 现在,包装器任务将始终观察其内部任务的任何异常。

An exception from a Task may or may not be marshalled back to the calling thread when using Wait with a timeout. 使用带有超时的等待时,可能会或可能不会将Task中的异常封送回调用线程。 If the exception occurs BEFORE the timeout for the Wait call, then the exception gets marshalled back to the calling thread. 如果异常发生在Wait调用超时之前,则该异常将编组回调用线程。 If the exception occurs AFTER the timeout for the Wait call, then the exception remains as an unobserved exception of the task. 如果在等待调用超时后发生异常,则该异常将保留为任务的未观察到异常。

I'd think of this in terms of where the exception is instead of "marshalling" or "calling threads". 我会从异常的位置而不是“编组”或“调用线程”的角度来考虑这一点。 That just confuses the issue. 那只会使问题困惑。 When a task faults, the exception is placed on the task (and the task is completed). 当任务发生故障时,该任务将被放置例外(并且该任务已完成)。 So if the exception is placed on the task before the wait completes, then the wait is satisfied by the task completing, and raises the exception. 因此,如果在等待完成之前将异常放置在任务上,则任务完成会满足等待,并引发异常。 If the wait times out before the task completes, then the wait is no longer waiting, so of course it doesn't see the exception, and the exception can possibly be unobserved. 如果等待在任务完成之前超时,则等待不再等待,因此当然看不到异常,并且该异常可能无法观察到。

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

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