我怎样才能打破ContinueWith中未处理的异常?

时间:2015-03-31 01:15:34

标签: c# .net exception task-parallel-library

当我编写基于任务的代码时,我的一些ContinueWith子句故意抛出异常(我捕获并正确处理),其中一些意外抛出异常(因为错误)。如何在第二种情况下避免破坏第一种?

在下面的代码中,我希望调试器在意外异常上中断,而不是在故意异常上(因为稍后会处理)。如果我禁用" Just My Code"根据{{​​3}},故意异常不会破坏调试器(正确),但无意的异常也不会破坏调试器(不正确)。如果我启用" Just My Code",那么无意识异常会破坏调试器(正确),但故意异常也是如此(不正确)。

是否有任何设置可以使ContinueWith子句中的异常像普通开发人员那样工作,可能会期望他们这样做?

class Program
{
    static void Main(string[] args)
    {
        //Intentional Exception
        Task.Factory
            .StartNew(() => Console.WriteLine("Basic action"))
            .ContinueWith((t1) => { throw new Exception("Intentional Exception"); })
            .ContinueWith((t2) => Console.WriteLine("Caught '" + t2.Exception.InnerException.Message + "'"));

        //Unintentional Exception
        Task.Factory
            .StartNew(() => Console.WriteLine("Basic action"))
            .ContinueWith((t3) => { throw new Exception("Unintentional Exception (bug)"); });

        Console.ReadLine();
    }
}

2 个答案:

答案 0 :(得分:1)

有两种可能的方法可以解决这个问题。它们都不是非常令人满意,因为它们基本上依赖于开发人员100%检测自己的错误,这实际上是调试器在开发环境中应该做的事情。对于这两种方法,开发人员应首先关闭“Just My Code”,因为这是防止捕获/观察到的故意异常导致调试器中断的方法。

首先,正如@Peter Deniho在his answer及其评论中提到的那样,开发人员可以监听TaskScheduler.UnobservedTaskException事件。这个代码很简单:

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    throw new Exception("An Exception occurred in a Task but was not observed", e.Exception.InnerException);
};

这种方法的难点在于此事件仅在调用故障Task的终结器时触发。这在垃圾收集期间发生,但垃圾收集可能不会发生,直到异常发生很久。要查看这是一个严重的问题,请使用本段后面提到的方法,但注释掉GC.Collect()。在那种情况下,Heartbeats将在未观察到的异常被抛出之前持续很长时间。要解决此问题,必须尽可能频繁地强制进行垃圾回收,以确保向开发人员报告未观察到的/无意的异常。这可以通过定义以下方法并在原始静态void Main的开头调用它来完成:

private static async void WatchForUnobservedExceptions()
{
    TaskScheduler.UnobservedTaskException += (sender, e) =>
    {
        throw new Exception("An Exception occurred in a Task but was not observed", e.Exception.InnerException);
    };

    //This polling loop is necessary to ensure the faulted Task's finalizer is called (which causes UnobservedTaskException to fire) in a timely fashion
    while (true)
    {
        Console.WriteLine("Heartbeat");
        GC.Collect();
        await Task.Delay(5000);
    }
}

当然,这会产生严重的性能影响,而且这是必要的pretty good indicator of fundamentally broken code

上述方法可以定性为定期轮询,以自动检查可能遗漏的任务异常。第二种方法是明确检查每个任务的异常。这意味着开发人员必须识别任何未查看异常的“后台”任务(通过await,Wait(),Result或Exception),并对未观察到的任何异常执行某些操作。这是我首选的扩展方法:

static class Extensions
{
    public static void CrashOnException(this Task task)
    {
        task.ContinueWith((t) =>
        {
            Console.WriteLine(t.Exception.InnerException);
            Debugger.Break();
            Console.WriteLine("The process encountered an unhandled Exception during Task execution.  See above trace for details.");
            Environment.Exit(-1);
        }, TaskContinuationOptions.OnlyOnFaulted);
    }
}

一旦定义,这个方法可以简单地附加到任何可能有未观察异常的任务,如果它有错误:

Task.Factory
    .StartNew(() => Console.WriteLine("Basic action"))
    .ContinueWith((t3) => { throw new Exception("Unintentional Exception (bug)"); })
    .CrashOnException();

这种方法的主要缺点是必须识别每个approriate任务(大多数任务不需要或不希望这种修改,因为大多数任务在程序的某个未来点使用),然后由开发人员加后缀。如果开发人员忘记使用未观察到的异常后缀任务,则会像原始问题中的无意异常行为一样静默地吞下它。这意味着这种方法在捕捉由于疏忽引起的错误方面效果较差,即使这样做是关于这个问题的。为了减少捕获错误的可能性,您不会受到第一种方法的影响。

答案 1 :(得分:0)

我很确定你不能在这里做你想做的事,不完全是。

注意:您的代码示例有点误导。文本This line should never print实际上永远不会打印,这是没有理由的。你不要以任何方式等待任务,所以输出发生在任务甚至足以引发异常之前。

这是一个代码示例,说明了您所询问的内容,但恕我直言的内容更为强大,这是展示所涉及的不同行为的更好起点:

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine();
    Console.WriteLine("Unobserved exception: " + e.Exception);
    Console.WriteLine();
};

//Intentional Exception
try
{
    Console.WriteLine("Test 1:");
    Task.Factory
            .StartNew(() => Console.WriteLine("Basic action"))
            .ContinueWith((t1) => { throw new Exception("Intentional Exception"); })
            .ContinueWith(t2 => Console.WriteLine("Caught '" +
                t2.Exception.InnerException.Message + "'"))
            .Wait();
}
catch (Exception e)
{
    Console.WriteLine("Exception: " + e);
}
Console.WriteLine();

//Unintentional Exception
try
{
    Console.WriteLine("Test 2:");
    Task.Factory
            .StartNew(() => Console.WriteLine("Basic action"))
            .ContinueWith(
                t3 => { throw new Exception("Unintentional Exception (bug)"); })
            .Wait();
}
catch (Exception e)
{
    Console.WriteLine("Exception: " + e);
}
Console.WriteLine();

Console.WriteLine("Done running tasks");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Done with GC.Collect() and finalizers");
Console.ReadLine();

根本问题在于,即使在您的第一个Task示例中,您也不是处理异常。 Task类已经做到了。你所做的只是通过添加延续,添加一个新的Task,它本身不会抛出异常,因为当然它不会抛出异常

如果您要等待原始延续(即抛出异常的那个),您会发现异常实际上并未处理。该任务仍将抛出异常:

Task task1 = Task.Factory
        .StartNew(() => Console.WriteLine("Basic action"))
        .ContinueWith(t1 => { throw new Exception("Intentional Exception"); });

Task task2 = task1
        .ContinueWith(t2 => Console.WriteLine("Caught '" +
            t2.Exception.InnerException.Message + "'"));

task2.Wait();
task1.Wait(); // exception thrown here

即。在上面,task2.Wait()很好,但task1.Wait()仍然会抛出异常。

Task类确实知道是否已“观察”异常。因此,即使您不等待第一个任务,只要您在延续中检索Exception属性(如您的示例所示),Task类就会很高兴。

但是请注意,如果更改延续以便它忽略Exception属性值并且等待抛出异常的任务,那么终结器在我的版本中运行时在您的示例中,您会发现报告了一个未观察到的任务异常。

Task task1 = Task.Factory
        .StartNew(() => Console.WriteLine("Basic action"))
        .ContinueWith(t1 => { throw new Exception("Intentional Exception"); });

Task task2 = task1
        .ContinueWith(t2 => Console.WriteLine("Caught exception"));

task2.Wait();
task1 = null; // ensure the object is in fact collected

那么,在你的问题中,这一切究竟意味着什么呢?好吧,对我而言,这意味着调试器必须在其中内置特殊逻辑才能识别这些场景并理解即使异常未被处理, 被“观察”每个Task类的规则。

我不知道会有多少工作,但听起来对我来说很多工作。此外,它将是调试器的一个示例,其中包括有关.NET中特定类的详细信息,我认为通常希望避免这种情况。无论如何,我怀疑工作已经完成。就调试器而言,在这两种情况下,你都有一个例外但你没有处理它。它无法分辨两者之间的区别。


现在,所有这一切......

在我看来,在您的真实代码中,您可能实际上并没有抛出普通的Exception类型。因此,您可以选择使用“Debug / Exceptions ...”对话框:

enter image description here

...启用或禁用特定异常中断。对于那些你知道的人,尽管它们实际上没有得到处理,但实际上是观察,因此当它们发生时你不需要打破调试器,你可以禁止打破这些异常。调试器。调试器仍将中断其他异常,但会忽略您已禁用的异常。

当然,如果您有可能意外地无法捕获/处理/观察您通常正常处理的异常之一,那么这不是一种选择。您只需要使用调试器中断并继续。

不理想,但坦率地说也不应该是一个大问题;毕竟,异常是例外情况的定义。它们不应该用于流控制或线程间通信,因此应该很少发生。大多数任务应该捕获他们自己的异常并优雅地处理它们,所以你不应该经常点击 F5