在异步方法中捕获异常后激活
Exceptions active after have been caught in async methods
现在我知道首先使用 Marshal.GetExceptionCode()
是一种 hack,但问题不在于它(Visual Studio 调试器也检测到一个活动异常)
private static async Task TestAsync()
{
Log("TestAsync.Before");
await HandleExceptionAsync();
Log("TestAsync.After");
}
private static async Task HandleExceptionAsync()
{
try
{
Log("HandleExceptionAsync.Try");
await ThrowAsync();
}
catch (InvalidOperationException)
{
Log("HandleExceptionAsync.Catch");
}
Log("HandleExceptionAsync.AfterCatch");
}
private static async Task ThrowAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("Delayed exception");
}
private static void Log(string step)
{
Console.WriteLine($"{step}: {Marshal.GetExceptionCode()}");
}
输出
TestAsync.Before: 0
HandleExceptionAsync.Try: 0
Exception thrown: 'System.InvalidOperationException' in Interactive.dll
Exception thrown: 'System.InvalidOperationException' in System.Private.CoreLib.ni.dll
HandleExceptionAsync.Catch: -532462766
HandleExceptionAsync.AfterCatch: -532462766
TestAsync.After: -532462766
The thread 9292 has exited with code 0 (0x0).
异常在整个等待链中保持活动状态,即使它已被捕获。
我检查了生成的代码,它没有给出为什么会发生这种情况的线索,相关部分(为 HandleExceptionAsync
状态机生成 MoveNext
):
void IAsyncStateMachine.MoveNext()
{
int num1 = this.\u003C\u003E1__state;
try
{
if (num1 == 0)
;
try
{
TaskAwaiter awaiter;
int num2;
if (num1 != 0)
{
Program.Log("HandleExceptionAsync.Try");
awaiter = Program.ThrowAsync().GetAwaiter();
if (!awaiter.IsCompleted)
{
this.\u003C\u003E1__state = num2 = 0;
this.\u003C\u003Eu__1 = awaiter;
Program.\u003CHandleExceptionAsync\u003Ed__1 stateMachine = this;
this.\u003C\u003Et__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.\u003CHandleExceptionAsync\u003Ed__1>(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = this.\u003C\u003Eu__1;
this.\u003C\u003Eu__1 = new TaskAwaiter();
this.\u003C\u003E1__state = num2 = -1;
}
awaiter.GetResult();
awaiter = new TaskAwaiter();
}
catch (InvalidOperationException ex)
{
Program.Log("HandleExceptionAsync.Catch");
}
Program.Log("HandleExceptionAsync.AfterCatch");
}
catch (Exception ex)
{
this.\u003C\u003E1__state = -2;
this.\u003C\u003Et__builder.SetException(ex);
return;
}
this.\u003C\u003E1__state = -2;
this.\u003C\u003Et__builder.SetResult();
}
我也没有看到这与同步上下文相关(在本例中它是一个控制台应用程序,因此在池中安排了延续),我最好的猜测是发生了一些调用堆栈操作,但我不能找到任何关于此的有用信息。
如果有人能解释为什么会发生这种情况并向文档提供 link 解释这是如何在 CLR/编译器中实现的,我将不胜感激
UPD1: 添加了 VS 调试器的屏幕截图,在异步中显示活动异常,在同步中不显示任何内容
异步
同步
如果你在 Log("HandleExceptionAsync.AfterCatch");
处放置一个断点,调用堆栈解释了技巧:
ConsoleApp1.exe!ConsoleApp1.Program.Log(string step) Line 107 C#
ConsoleApp1.exe!ConsoleApp1.Program.HandleExceptionAsync() Line 95 C#
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object stateMachine) Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run() Unknown
mscorlib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining, ref System.Threading.Tasks.Task currentTask) Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations() Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageTwo() Unknown
mscorlib.dll!System.Threading.Tasks.Task.Finish(bool bUserDelegateExecuted) Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.SetException(System.Exception exception) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetException(System.Exception exception) Unknown
ConsoleApp1.exe!ConsoleApp1.Program.ThrowAsync() Line 101 C#
... (continues until the timer of Task.Delay)
看到底框了吗?我们仍在 ThrowAsync 中,即使我们是从 HandleExceptionAsync
记录的。这怎么可能?答案也在调用栈中:
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject) Unknown
简单来说,由于 await
关键字,您的 HandleExceptionAsync
方法被截断如下:
void HandleExceptionAsync1()
{
Log("HandleExceptionAsync.Try");
}
void HandleExceptionAsync2()
{
Log("HandleExceptionAsync.AfterCatch");
}
当然,这比那要复杂得多。事实上,该方法并没有被切碎,而是被简单地转换成一个状态机。然而,对于这个演示,这在理论上是等效的。
HandleExceptionAsync2
需要在ThrowAsync
之后执行。因此,HandleExceptionAsync2
将被链接为一个延续。类似于:
ThrowAsync().ContinueWith(HandleExceptionAsync2);
(同样,这比那个复杂得多。我只是为了解释而简化)
"problem"是,当运行时完成ThrowAsync返回的任务时:
System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject)
continuation 实际上是内联的,并在同一个调用堆栈中执行(参见上面的框架)。出于性能原因,这是 TPL 经常进行的优化。因此,在调用 Log("HandleExceptionAsync.AfterCatch");
、 时,您实际上仍在 ThrowAsync
的 catch 块中,因此您会看到这种行为。
现在我知道首先使用 Marshal.GetExceptionCode()
是一种 hack,但问题不在于它(Visual Studio 调试器也检测到一个活动异常)
private static async Task TestAsync()
{
Log("TestAsync.Before");
await HandleExceptionAsync();
Log("TestAsync.After");
}
private static async Task HandleExceptionAsync()
{
try
{
Log("HandleExceptionAsync.Try");
await ThrowAsync();
}
catch (InvalidOperationException)
{
Log("HandleExceptionAsync.Catch");
}
Log("HandleExceptionAsync.AfterCatch");
}
private static async Task ThrowAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("Delayed exception");
}
private static void Log(string step)
{
Console.WriteLine($"{step}: {Marshal.GetExceptionCode()}");
}
输出
TestAsync.Before: 0
HandleExceptionAsync.Try: 0
Exception thrown: 'System.InvalidOperationException' in Interactive.dll
Exception thrown: 'System.InvalidOperationException' in System.Private.CoreLib.ni.dll
HandleExceptionAsync.Catch: -532462766
HandleExceptionAsync.AfterCatch: -532462766
TestAsync.After: -532462766
The thread 9292 has exited with code 0 (0x0).
异常在整个等待链中保持活动状态,即使它已被捕获。
我检查了生成的代码,它没有给出为什么会发生这种情况的线索,相关部分(为 HandleExceptionAsync
状态机生成 MoveNext
):
void IAsyncStateMachine.MoveNext()
{
int num1 = this.\u003C\u003E1__state;
try
{
if (num1 == 0)
;
try
{
TaskAwaiter awaiter;
int num2;
if (num1 != 0)
{
Program.Log("HandleExceptionAsync.Try");
awaiter = Program.ThrowAsync().GetAwaiter();
if (!awaiter.IsCompleted)
{
this.\u003C\u003E1__state = num2 = 0;
this.\u003C\u003Eu__1 = awaiter;
Program.\u003CHandleExceptionAsync\u003Ed__1 stateMachine = this;
this.\u003C\u003Et__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.\u003CHandleExceptionAsync\u003Ed__1>(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = this.\u003C\u003Eu__1;
this.\u003C\u003Eu__1 = new TaskAwaiter();
this.\u003C\u003E1__state = num2 = -1;
}
awaiter.GetResult();
awaiter = new TaskAwaiter();
}
catch (InvalidOperationException ex)
{
Program.Log("HandleExceptionAsync.Catch");
}
Program.Log("HandleExceptionAsync.AfterCatch");
}
catch (Exception ex)
{
this.\u003C\u003E1__state = -2;
this.\u003C\u003Et__builder.SetException(ex);
return;
}
this.\u003C\u003E1__state = -2;
this.\u003C\u003Et__builder.SetResult();
}
我也没有看到这与同步上下文相关(在本例中它是一个控制台应用程序,因此在池中安排了延续),我最好的猜测是发生了一些调用堆栈操作,但我不能找到任何关于此的有用信息。
如果有人能解释为什么会发生这种情况并向文档提供 link 解释这是如何在 CLR/编译器中实现的,我将不胜感激
UPD1: 添加了 VS 调试器的屏幕截图,在异步中显示活动异常,在同步中不显示任何内容
异步
同步
如果你在 Log("HandleExceptionAsync.AfterCatch");
处放置一个断点,调用堆栈解释了技巧:
ConsoleApp1.exe!ConsoleApp1.Program.Log(string step) Line 107 C#
ConsoleApp1.exe!ConsoleApp1.Program.HandleExceptionAsync() Line 95 C#
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object stateMachine) Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run() Unknown
mscorlib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining, ref System.Threading.Tasks.Task currentTask) Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations() Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageTwo() Unknown
mscorlib.dll!System.Threading.Tasks.Task.Finish(bool bUserDelegateExecuted) Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.SetException(System.Exception exception) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetException(System.Exception exception) Unknown
ConsoleApp1.exe!ConsoleApp1.Program.ThrowAsync() Line 101 C#
... (continues until the timer of Task.Delay)
看到底框了吗?我们仍在 ThrowAsync 中,即使我们是从 HandleExceptionAsync
记录的。这怎么可能?答案也在调用栈中:
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject) Unknown
简单来说,由于 await
关键字,您的 HandleExceptionAsync
方法被截断如下:
void HandleExceptionAsync1()
{
Log("HandleExceptionAsync.Try");
}
void HandleExceptionAsync2()
{
Log("HandleExceptionAsync.AfterCatch");
}
当然,这比那要复杂得多。事实上,该方法并没有被切碎,而是被简单地转换成一个状态机。然而,对于这个演示,这在理论上是等效的。
HandleExceptionAsync2
需要在ThrowAsync
之后执行。因此,HandleExceptionAsync2
将被链接为一个延续。类似于:
ThrowAsync().ContinueWith(HandleExceptionAsync2);
(同样,这比那个复杂得多。我只是为了解释而简化)
"problem"是,当运行时完成ThrowAsync返回的任务时:
System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject)
continuation 实际上是内联的,并在同一个调用堆栈中执行(参见上面的框架)。出于性能原因,这是 TPL 经常进行的优化。因此,在调用 Log("HandleExceptionAsync.AfterCatch");
、 时,您实际上仍在 ThrowAsync
的 catch 块中,因此您会看到这种行为。