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

标签 c# multithreading error-handling task-parallel-library task

等待超时后使用 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());
        }

因为从未观察到异常,因此没有适本地记录,这将不起作用。

此信息导致以下规则:
  • 不要从 .ContinueWith 抛出。从这里抛出的异常不会被编码(marshal)回调用线程。这些异常仍然是未观察到的异常,并且被有效地吃掉了。
  • 当使用带有超时的等待时,来自任务的异常可能会也可能不会被编码回调用线程。如果在等待调用超时之前发生异常,则异常将被编码回调用线程。如果在 Wait 调用超时之后发生异常,则该异常仍然是任务的未观察到的异常。

  • 规则 #2 非常丑陋。在这种情况下,我们如何可靠地记录异常?我们可以使用 .ContinueWith/OnlyOnFaulted 来记录异常(见下文)。
            task.ContinueWith(antecedent =>
            {
                Logger.Log(antecedent.Exception.ToString());
                //throw new CustomException();
            }, TaskContinuationOptions.OnlyOnFaulted);
    

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

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

    最佳答案

    Don’t throw from .ContinueWith. Exceptions thrown from here aren’t marshaled back to the calling thread. These exceptions remain as unobserved exceptions and are effectively eaten.



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

    我想说更合适的建议是 not use ContinueWith at all (有关详细信息,请参阅我的博客)。一旦你改为使用 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(() => ...
    

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

    An exception from a Task may or may not be marshalled back to the calling thread when using Wait with a timeout. If the exception occurs BEFORE the timeout for the Wait call, then the exception gets marshalled back to the calling thread. If the exception occurs AFTER the timeout for the Wait call, then the exception remains as an unobserved exception of the task.



    我会根据异常在哪里而不是“编码”或“调用线程”来考虑这一点。这只是混淆了这个问题。当一个任务发生故障时,该异常被放置在该任务上(并且该任务完成)。因此,如果在等待完成之前将异常放置在任务上,则等待由任务完成来满足,并引发异常。如果在任务完成之前等待就超时了,那么等待就不再等待了,所以它当然看不到异常,也有可能没有观察到异常。

    关于等待超时后使用 Task.Wait 记录 C#/Tasks 错误两次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39452667/

    相关文章:

    c# - Dotnet publish 不发布 DLL 到发布目录

    c# - 我应该用 C/C++ 重写我的 DSP 例程,还是我擅长使用 C# 不安全指针?

    c# - WCF 服务和单例

    multithreading - 如果posix互斥锁被解锁同时锁定了多个线程,会发生什么情况?

    协程中的 Python 循环

    sql - SQL : cleaning a DB, checking that there's no violation and maybe rolling back

    c# - C#、.NET 中的 Google Api OAuth

    c++ - 用于时区规则的 Linux/跨平台 API? (替换锁定 localtime_r)

    asp.net - 在ASP.net中显示SQL错误

    java - 我们如何处理异步 Web 服务调用的异常?