以通用方式在委托内部捕获异常
Catching Exceptions INSIDE a delegate in a generic fashion
我需要帮助确定一种通用方法来捕获从委托中抛出的异常,当该委托包含一个正在抛出异常的 Task.Run 时。目前,当我调用传递给另一个 "master" 方法的委托(在本例中为 func)并且从委托中的 Task.Run 中抛出异常时,异常不会被 try/catch 在围绕调用的代码中。以下简化示例显示了委托内的异常被委托内的 try/catch 捕获但在其他地方被 "lost" 捕获:
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += (s, e) => CurrentDomain_UnhandledException();
TaskScheduler.UnobservedTaskException += (s, e) => CurrentDomain_UnhandledException();
HandledDelegate(() => Task.Run(() =>
{
try
{
Console.WriteLine("Executed");
throw new Exception($"Caught");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw new Exception($"Caught Again");
}
}
));
}
private static void CurrentDomain_UnhandledException()
{
Console.WriteLine("Unhandled Exception Occurred.");
}
public static void HandledDelegate(Action a)
{
try
{
a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
Thread.Sleep(10000);
}
下面的示例显示异常未在调用周围的 try/catch 中捕获,并作为未处理的异常冒泡到应用程序域。
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += (s, e) => Console.WriteLine("Unhandled Exception");
HandleDelegateEx(async () => await Task.Run(() => throw new Exception("test")));
Thread.Sleep(1000);
}
public static void HandleDelegateEx(Action a)
{
try
{
a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
这最后一个最像我需要修复的当前代码。委托通常在后台线程上关闭代码任务并且不处理任何错误(期望它们在委托被调用的地方被捕获。
此代码在整个应用程序的数百个位置使用,很少(如果有的话)捕获委托中的异常(因为其他程序员错误地认为会捕获委托抛出的异常)。有没有什么方法可以捕获从这些委托内部抛出的异常,而无需大量重构每个委托来捕获异常?
问题与委托本身无关。在您的 fiddle 中,您不必等待委托人完成 Task
return。基本上委托将 return Task
并且该方法将继续。在您的原始代码中:
try
{
// Execute the requested action.
retval = clientOperation.Invoke(remoteObj);
}
catch(ex)
{
// log here.
}
finally
{
CloseChannel(remoteObj);
}
它将直接跳转到finally 子句并尝试关闭连接。您需要单独处理 Func
的 returning 任务:
public void HandleDelegateEx<T>(Func<T> a)
{
try
{
a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public async Task HandleDelegateExAsync<T>(Func<Task<T>> a)
{
try
{
await a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
HandleDelegateEx<int>(() => throw new Exception("test")); // prints "test"
Func<Task<int>> func = () => Task.FromException<int>(new Exception("test async"));
HandleDelegateEx(func); // does not print anything
await HandleDelegateExAsync(func); // prints "test async"
你可以在没有委托的情况下获得相同的行为:
Task task = null;
try
{
task = Task.FromException(new Exception("test async"));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // does not go here
}
await task; // throws here
为了让您的异常被观察到,您要么必须按照 Guru Stron 的建议等待 returned 任务(我猜这意味着大规模重构) .或者以某种方式将 Wait()
添加到 Task.Run()
调用的结果中,但是 这将阻止调用线程 (我假设这是UI thread) 而后台线程搅动委托 - 首先忽略了任务作为后台线程的好处。
所以,最有可能的是,问题的症结在于:您需要等待后台任务完成,以便您可以观察任何可能的异常,而不会阻塞 UI 线程,并在修改最少代码量的同时执行此操作。
我假设您的应用程序是 Windows Forms 或 WPF。
对于第一个问题——等待任务完成而不阻塞 UI 线程——,我会求助于旧的(并且非常不受欢迎)Application.DoEvents()
(or its equivalent in WPF, second answer)。最近 强烈 不鼓励这样做,但我想这比忽略本应处理的异常要好得多(请记住这是有效的,事实上,这是保持异常的唯一方法responsive UI 在 16 位时代)。
我不会将 Wait()
添加到后台任务,而是使用以下循环等待其完成:
while( !backgroundTask.IsCompleted ) {
Application.DoEvents(); // keep UI responsive while background task finishes
}
现在,第二个问题,我们必须在后台任务在线程池中排队后立即以某种方式注入此循环,正如您所描述的,这可能发生在数百个地方。
这里我会尝试使用Harmony, a very powerful dynamic IL patching tool。此工具允许您以方法为目标并搭载前缀 and/or 后缀方法。调用目标方法前会自动调用前缀方法,目标方法调用后会自动调用后缀方法
补丁看起来像这样:
// Must be called once before the target delegates are executed
private void MyClass.Patch() {
var original = typeof(Task).GetMethod("Run", ...); // I'm omitting the code that would select the (Action action) overload
var postfix = typeof(MyClass).GetMethod("WaitWithResponsiveUI", ...);
harmony.Patch(original, postfix: new HarmonyMethod(postfix));
}
private void MyClass.WaitWithResponsiveUI(ref Action __result) {
// __result contains the original Task returned by Task.Run()
while( !__result.IsCompleted ) {
Application.DoEvents(); // keep UI responsive while background task finishes
}
__result.Wait(); // Any exceptions produced inside the delegate should be rethrown at this moment, and they should be trappable by the code that invoked the delegate
}
当然,这有一个缺点,即修补后调用的每个 Task.Run(Action action)
现在都会等到任务完成,全局地,这可能不是每次调用都需要的。
编辑:为 Application.DoEvents();
辩护
这里有一个 very well written answer 详细说明了使用 Application.DoEvents()
的所有潜在陷阱。 这些陷阱是真实存在的,应该加以考虑。
然而,我强烈反对的一个方面是断言 async-await 机制是避免所有这些问题的安全方法。 这根本不是真的。
当一个 UI 事件处理程序找到一个 await
时,它基本上建立了一个延续,可以在将来恢复事件处理,并且 立即 returns,让应用程序的消息泵有机会继续 运行(基本上是 Application.DoEvents()
,不同之处在于后者将 return 一旦事件队列是空的)。
当等待的操作完成时,继续将作为另一个事件排队到消息事件队列中,因此在消息泵处理完之前发布的所有事件之前它不会恢复执行。我的观点是 - 在等待操作的同时,正在处理消息,您仍然需要手动保护您的应用程序免受任何重入陷阱。参见此处:Handling Reentrancy in Async Apps (C#).
async-await UI code 独有的另一个陷阱是某些组件(想到 DataGridView)将它们的一些事件参数传递给对象中的处理程序实施 IDisposable
。组件引发事件,并在事件处理程序 return 后立即处理参数。 在异步代码中,当到达处理程序代码中的第一个 await
时,将向调用者发送此 return。这具有这样的效果,当等待的代码恢复时,访问事件参数对象会抛出一个 ObjectDisposedException
,因为它已经被引发组件本身处理掉了!
"false multithreading" 方面已经在这里处理,因为工作真正被卸载到线程池。
因此,在我看来,唯一真正需要解决的问题是潜在的堆栈溢出问题,可以通过必须首先考虑的重入避免措施来解决,无论您使用 async/await 或 DoEvents()
.
我需要帮助确定一种通用方法来捕获从委托中抛出的异常,当该委托包含一个正在抛出异常的 Task.Run 时。目前,当我调用传递给另一个 "master" 方法的委托(在本例中为 func)并且从委托中的 Task.Run 中抛出异常时,异常不会被 try/catch 在围绕调用的代码中。以下简化示例显示了委托内的异常被委托内的 try/catch 捕获但在其他地方被 "lost" 捕获:
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += (s, e) => CurrentDomain_UnhandledException();
TaskScheduler.UnobservedTaskException += (s, e) => CurrentDomain_UnhandledException();
HandledDelegate(() => Task.Run(() =>
{
try
{
Console.WriteLine("Executed");
throw new Exception($"Caught");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw new Exception($"Caught Again");
}
}
));
}
private static void CurrentDomain_UnhandledException()
{
Console.WriteLine("Unhandled Exception Occurred.");
}
public static void HandledDelegate(Action a)
{
try
{
a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
Thread.Sleep(10000);
}
下面的示例显示异常未在调用周围的 try/catch 中捕获,并作为未处理的异常冒泡到应用程序域。
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += (s, e) => Console.WriteLine("Unhandled Exception");
HandleDelegateEx(async () => await Task.Run(() => throw new Exception("test")));
Thread.Sleep(1000);
}
public static void HandleDelegateEx(Action a)
{
try
{
a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
这最后一个最像我需要修复的当前代码。委托通常在后台线程上关闭代码任务并且不处理任何错误(期望它们在委托被调用的地方被捕获。
此代码在整个应用程序的数百个位置使用,很少(如果有的话)捕获委托中的异常(因为其他程序员错误地认为会捕获委托抛出的异常)。有没有什么方法可以捕获从这些委托内部抛出的异常,而无需大量重构每个委托来捕获异常?
问题与委托本身无关。在您的 fiddle 中,您不必等待委托人完成 Task
return。基本上委托将 return Task
并且该方法将继续。在您的原始代码中:
try
{
// Execute the requested action.
retval = clientOperation.Invoke(remoteObj);
}
catch(ex)
{
// log here.
}
finally
{
CloseChannel(remoteObj);
}
它将直接跳转到finally 子句并尝试关闭连接。您需要单独处理 Func
的 returning 任务:
public void HandleDelegateEx<T>(Func<T> a)
{
try
{
a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public async Task HandleDelegateExAsync<T>(Func<Task<T>> a)
{
try
{
await a.Invoke();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
HandleDelegateEx<int>(() => throw new Exception("test")); // prints "test"
Func<Task<int>> func = () => Task.FromException<int>(new Exception("test async"));
HandleDelegateEx(func); // does not print anything
await HandleDelegateExAsync(func); // prints "test async"
你可以在没有委托的情况下获得相同的行为:
Task task = null;
try
{
task = Task.FromException(new Exception("test async"));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // does not go here
}
await task; // throws here
为了让您的异常被观察到,您要么必须按照 Guru Stron 的建议等待 returned 任务(我猜这意味着大规模重构) .或者以某种方式将 Wait()
添加到 Task.Run()
调用的结果中,但是 这将阻止调用线程 (我假设这是UI thread) 而后台线程搅动委托 - 首先忽略了任务作为后台线程的好处。
所以,最有可能的是,问题的症结在于:您需要等待后台任务完成,以便您可以观察任何可能的异常,而不会阻塞 UI 线程,并在修改最少代码量的同时执行此操作。
我假设您的应用程序是 Windows Forms 或 WPF。
对于第一个问题——等待任务完成而不阻塞 UI 线程——,我会求助于旧的(并且非常不受欢迎)Application.DoEvents()
(or its equivalent in WPF, second answer)。最近 强烈 不鼓励这样做,但我想这比忽略本应处理的异常要好得多(请记住这是有效的,事实上,这是保持异常的唯一方法responsive UI 在 16 位时代)。
我不会将 Wait()
添加到后台任务,而是使用以下循环等待其完成:
while( !backgroundTask.IsCompleted ) {
Application.DoEvents(); // keep UI responsive while background task finishes
}
现在,第二个问题,我们必须在后台任务在线程池中排队后立即以某种方式注入此循环,正如您所描述的,这可能发生在数百个地方。
这里我会尝试使用Harmony, a very powerful dynamic IL patching tool。此工具允许您以方法为目标并搭载前缀 and/or 后缀方法。调用目标方法前会自动调用前缀方法,目标方法调用后会自动调用后缀方法
补丁看起来像这样:
// Must be called once before the target delegates are executed
private void MyClass.Patch() {
var original = typeof(Task).GetMethod("Run", ...); // I'm omitting the code that would select the (Action action) overload
var postfix = typeof(MyClass).GetMethod("WaitWithResponsiveUI", ...);
harmony.Patch(original, postfix: new HarmonyMethod(postfix));
}
private void MyClass.WaitWithResponsiveUI(ref Action __result) {
// __result contains the original Task returned by Task.Run()
while( !__result.IsCompleted ) {
Application.DoEvents(); // keep UI responsive while background task finishes
}
__result.Wait(); // Any exceptions produced inside the delegate should be rethrown at this moment, and they should be trappable by the code that invoked the delegate
}
当然,这有一个缺点,即修补后调用的每个 Task.Run(Action action)
现在都会等到任务完成,全局地,这可能不是每次调用都需要的。
编辑:为 Application.DoEvents();
辩护这里有一个 very well written answer 详细说明了使用 Application.DoEvents()
的所有潜在陷阱。 这些陷阱是真实存在的,应该加以考虑。
然而,我强烈反对的一个方面是断言 async-await 机制是避免所有这些问题的安全方法。 这根本不是真的。
当一个 UI 事件处理程序找到一个 await
时,它基本上建立了一个延续,可以在将来恢复事件处理,并且 立即 returns,让应用程序的消息泵有机会继续 运行(基本上是 Application.DoEvents()
,不同之处在于后者将 return 一旦事件队列是空的)。
当等待的操作完成时,继续将作为另一个事件排队到消息事件队列中,因此在消息泵处理完之前发布的所有事件之前它不会恢复执行。我的观点是 - 在等待操作的同时,正在处理消息,您仍然需要手动保护您的应用程序免受任何重入陷阱。参见此处:Handling Reentrancy in Async Apps (C#).
async-await UI code 独有的另一个陷阱是某些组件(想到 DataGridView)将它们的一些事件参数传递给对象中的处理程序实施 IDisposable
。组件引发事件,并在事件处理程序 return 后立即处理参数。 在异步代码中,当到达处理程序代码中的第一个 await
时,将向调用者发送此 return。这具有这样的效果,当等待的代码恢复时,访问事件参数对象会抛出一个 ObjectDisposedException
,因为它已经被引发组件本身处理掉了!
"false multithreading" 方面已经在这里处理,因为工作真正被卸载到线程池。
因此,在我看来,唯一真正需要解决的问题是潜在的堆栈溢出问题,可以通过必须首先考虑的重入避免措施来解决,无论您使用 async/await 或 DoEvents()
.