如何仅在 ContinueWith 中出现未处理的异常时中断?
How can I break only on unhandled Exceptions in ContinueWith?
当我编写基于任务的代码时,我的一些 ContinueWith 子句有意抛出异常(我捕获并适当处理),而其中一些不小心抛出异常(由于错误)。如何避免在第一种情况下中断,同时仍然在第二种情况下中断?
在下面的代码中,我希望调试器在无意异常时中断,而不是在有意异常时中断(因为稍后会处理)。如果我根据 this question 禁用 "Just My Code",则有意异常不会破坏调试器(正确),但无意异常也不会破坏调试器(不正确)。如果我启用 "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();
}
}
我很确定你不能在这里做你想做的事,不完全是。
注意:您的代码示例有点误导。文本 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
class 已经做到了。你所做的就是,通过添加延续,添加一个新的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
class 确实有异常是否已经 "observed" 的想法。因此,即使您不等待第一个任务,只要您在延续中检索 Exception
属性(如您的示例),Task
class很开心。
但请注意,如果您更改延续以使其忽略 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
那么在您的问题中,所有这些实际上意味着什么?好吧,对我来说,这意味着调试器必须内置特殊逻辑才能识别这些场景并理解即使未处理异常,它 是 "observed" 根据 Task
class.
的规则
我不知道那会有多少工作量,但听起来对我来说是很多工作量。此外,这将是调试器的一个示例,其中包含有关 .NET 中特定 class 的详细信息,我认为通常需要避免这种情况。无论如何,我怀疑工作是否已经完成。就调试器而言,在这两种情况下,您都会遇到异常并且您没有处理它。它无法分辨两者之间的区别。
现在,说了这么多……
在我看来,在您的真实代码中,您可能实际上并没有抛出普通的 Exception
类型。所以您可以选择使用 "Debug/Exceptions…" 对话框:
…启用或禁用特定异常中断。对于那些你知道的,尽管它们实际上没有被处理,但实际上是 observed 因此当它们发生时你不需要在调试器中中断,你可以禁用中断那些调试器中的异常。调试器仍会中断其他异常,但会忽略您已禁用的异常。
当然,如果您有可能不小心 catch/handle/observe 您通常可以正确处理的异常之一,那么这不是一个选择。您只需要忍受调试器中断并继续。
不理想,但坦率地说也不应该是一个大问题;毕竟,根据定义,例外是 例外 情况。它们不应该用于流量控制或线程间通信,因此应该很少出现。大多数任务应该捕获自己的异常并妥善处理它们,因此您不必经常点击 F5。
有两种可能的方法可以解决此问题。两者都不是很令人满意,因为它们基本上依赖于开发人员检测 100% 自己的错误,这实际上是调试器在开发环境中应该做的事情。对于任何一种方法,开发人员都应首先关闭 "Just My Code",因为这是防止 caught/observed 故意异常导致调试器中断的方法。
首先,正如@Peter Deniho 在 及其评论中提到的那样,开发人员可以监听TaskScheduler.UnobservedTaskException 事件。代码很简单:
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
throw new Exception("An Exception occurred in a Task but was not observed", e.Exception.InnerException);
};
这种方法的困难在于,只有在调用故障任务的终结器时才会触发此事件。这发生在垃圾回收期间,但垃圾回收可能要到异常发生很久之后才会发生。要看出这是一个严重的问题,请使用本段后面提到的方法,但注释掉 GC.Collect()。在那种情况下,在抛出未观察到的异常之前,心跳将持续很长时间。要解决此问题,必须尽可能频繁地强制执行垃圾收集,以确保将 unobserved/Unintentional 异常报告给开发人员。这可以通过定义以下方法并在原始 static 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。
上述方法的特点是定期轮询以自动检查可能遗漏的任务异常。第二种方法是显式检查每个任务的异常。这意味着开发人员必须识别任何 "background" 从未观察到异常(通过 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();
此方法的主要缺点是必须确定每个适当的任务(大多数任务不需要或不需要此修改,因为大多数任务会在程序中的某个未来点使用),然后由开发人员添加后缀。如果开发者忘记给一个 Task 加上 unobserved Exception 的后缀,它就会像原题中 Unintentional Exception 的行为一样被默默地吞掉。这意味着这种方法由于粗心大意而无法有效地捕获错误,即使这样做就是这个问题的内容。为了降低发现错误的可能性,您不会受到第一种方法的性能影响。
当我编写基于任务的代码时,我的一些 ContinueWith 子句有意抛出异常(我捕获并适当处理),而其中一些不小心抛出异常(由于错误)。如何避免在第一种情况下中断,同时仍然在第二种情况下中断?
在下面的代码中,我希望调试器在无意异常时中断,而不是在有意异常时中断(因为稍后会处理)。如果我根据 this question 禁用 "Just My Code",则有意异常不会破坏调试器(正确),但无意异常也不会破坏调试器(不正确)。如果我启用 "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();
}
}
我很确定你不能在这里做你想做的事,不完全是。
注意:您的代码示例有点误导。文本 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
class 已经做到了。你所做的就是,通过添加延续,添加一个新的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
class 确实有异常是否已经 "observed" 的想法。因此,即使您不等待第一个任务,只要您在延续中检索 Exception
属性(如您的示例),Task
class很开心。
但请注意,如果您更改延续以使其忽略 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
那么在您的问题中,所有这些实际上意味着什么?好吧,对我来说,这意味着调试器必须内置特殊逻辑才能识别这些场景并理解即使未处理异常,它 是 "observed" 根据 Task
class.
我不知道那会有多少工作量,但听起来对我来说是很多工作量。此外,这将是调试器的一个示例,其中包含有关 .NET 中特定 class 的详细信息,我认为通常需要避免这种情况。无论如何,我怀疑工作是否已经完成。就调试器而言,在这两种情况下,您都会遇到异常并且您没有处理它。它无法分辨两者之间的区别。
现在,说了这么多……
在我看来,在您的真实代码中,您可能实际上并没有抛出普通的 Exception
类型。所以您可以选择使用 "Debug/Exceptions…" 对话框:
…启用或禁用特定异常中断。对于那些你知道的,尽管它们实际上没有被处理,但实际上是 observed 因此当它们发生时你不需要在调试器中中断,你可以禁用中断那些调试器中的异常。调试器仍会中断其他异常,但会忽略您已禁用的异常。
当然,如果您有可能不小心 catch/handle/observe 您通常可以正确处理的异常之一,那么这不是一个选择。您只需要忍受调试器中断并继续。
不理想,但坦率地说也不应该是一个大问题;毕竟,根据定义,例外是 例外 情况。它们不应该用于流量控制或线程间通信,因此应该很少出现。大多数任务应该捕获自己的异常并妥善处理它们,因此您不必经常点击 F5。
有两种可能的方法可以解决此问题。两者都不是很令人满意,因为它们基本上依赖于开发人员检测 100% 自己的错误,这实际上是调试器在开发环境中应该做的事情。对于任何一种方法,开发人员都应首先关闭 "Just My Code",因为这是防止 caught/observed 故意异常导致调试器中断的方法。
首先,正如@Peter Deniho 在
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
throw new Exception("An Exception occurred in a Task but was not observed", e.Exception.InnerException);
};
这种方法的困难在于,只有在调用故障任务的终结器时才会触发此事件。这发生在垃圾回收期间,但垃圾回收可能要到异常发生很久之后才会发生。要看出这是一个严重的问题,请使用本段后面提到的方法,但注释掉 GC.Collect()。在那种情况下,在抛出未观察到的异常之前,心跳将持续很长时间。要解决此问题,必须尽可能频繁地强制执行垃圾收集,以确保将 unobserved/Unintentional 异常报告给开发人员。这可以通过定义以下方法并在原始 static 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。
上述方法的特点是定期轮询以自动检查可能遗漏的任务异常。第二种方法是显式检查每个任务的异常。这意味着开发人员必须识别任何 "background" 从未观察到异常(通过 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();
此方法的主要缺点是必须确定每个适当的任务(大多数任务不需要或不需要此修改,因为大多数任务会在程序中的某个未来点使用),然后由开发人员添加后缀。如果开发者忘记给一个 Task 加上 unobserved Exception 的后缀,它就会像原题中 Unintentional Exception 的行为一样被默默地吞掉。这意味着这种方法由于粗心大意而无法有效地捕获错误,即使这样做就是这个问题的内容。为了降低发现错误的可能性,您不会受到第一种方法的性能影响。