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

时间:2016-09-12 14:25:43

标签: 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());
        }

因为没有观察到异常,因此没有适当记录,这将无效。

此信息导致以下规则:

  1. 不要从.ContinueWith投掷。从这里抛出的异常不会被封送回调用线程。这些例外仍然是未被观察到的例外,并且被有效地吃掉。
  2. 当使用等待超时时,来自任务的异常可能会或可能不会被编组回调用线程。如果在Wait调用超时之前发生异常,则异常会被编组回调用线程。如果在Wait调用超时后发生异常,则异常仍然是任务的未观察异常。
  3. 规则#2非常难看。在这种情况下,我们如何可靠地记录异常?我们可以使用.ContinueWith / OnlyOnFaulted来记录异常(见下文)。

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

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

    我必须在这里找到一些东西。任何帮助将不胜感激。

1 个答案:

答案 0 :(得分:0)

  

不要从.ContinueWith投掷。从这里抛出的异常不会被封送回调用线程。这些例外仍然是未被观察到的例外,并且被有效地吃掉。

未观察到的异常的原因是因为未观察到任务(不是因为它是使用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模式更清楚地说明了这里真正发生的事情:代码创建的包装器任务必须用作< em>替换用于原始任务:

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

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

  

当使用等待超时时,来自任务的异常可能会或可能不会被编组回调用线程。如果在Wait调用超时之前发生异常,则异常会被编组回调用线程。如果在Wait调用超时后发生异常,则异常仍然是任务的未观察异常。

我在方面考虑到这一点,其中例外是而不是&#34;编组&#34;或者&#34;调用线程&#34;。这只是混淆了这个问题。当任务发生故障时,异常将放在任务上(并且任务完成)。因此,如果在等待完成之前将异常置于任务上,则任务完成将满足等待,并引发异常。如果在任务完成之前等待超时,则等待不再等待,因此它当然不会看到异常,并且可能无法观察到异常。