Vac*_*ano 5 c# exception async-await .net-core .net-core-3.1
我对我在 .Net Core 库中看到的异常行为感到困惑。这个问题的目标是了解为什么它正在做我所看到的。
我认为当一个async方法被调用时,其中的代码会同步执行,直到遇到第一个 await。如果是这种情况,那么,如果在该“同步代码”期间抛出异常,为什么它不会传播到调用方法?(就像普通的同步方法一样。)
给定 .Net Core 控制台应用程序中的以下代码:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
try
{
NonAwaitedMethod();
}
catch (Exception e)
{
Console.WriteLine("Exception Caught");
}
Console.ReadKey();
}
public static async Task NonAwaitedMethod()
{
Task startupDone = new Task(() => { });
var runTask = DoStuff(() =>
{
startupDone.Start();
});
var didStartup = startupDone.Wait(1000);
if (!didStartup)
{
throw new ApplicationException("Fail One");
}
await runTask;
}
public static async Task DoStuff(Action action)
{
// Simulate starting up blocking
var blocking = 100000;
await Task.Delay(500 + blocking);
action();
// Do the rest of the stuff...
await Task.Delay(3000);
}
Run Code Online (Sandbox Code Playgroud)
}
当按原样运行时,此代码将抛出异常,但是,除非您在其上设置断点,否则您不会知道它。Visual Studio 调试器和控制台将给出任何存在问题的指示(除了输出屏幕中的一行注释)。
交换NonAwaitedMethodfrom的返回类型Task到void。这将导致 Visual Studio 调试器现在因异常而中断。它也会在控制台中打印出来。但值得注意的是,异常不是在夹catch中找到的语句Main。
保留NonAwaitedMethodas的返回类型void,但去掉async. 还将最后一行从 更改await runTask;为runTask.Wait();(这实际上删除了任何异步内容。)运行时,异常会catch在Main方法的语句中捕获。
所以,总结一下:
| Scenario | Caught By Debugger | Caught by Catch |
|------------|--------------------|-----------------|
| async Task | No | No |
| async void | Yes | No |
| void | N/A | Yes |
Run Code Online (Sandbox Code Playgroud)
我认为因为异常是在await完成之前抛出的,所以它会同步执行,直到抛出异常为止。
因此我的问题是:为什么场景 1 或 2 都没有被catch语句捕获?
另外,为什么从交换Task到void返回类型原因异常的话,被调试器捕捉?(即使我没有使用该返回类型。)
在等待完成之前抛出异常,它将同步执行
认为这是相当正确的,但这并不意味着您可以捕获异常。
因为您的代码具有async关键字,它将方法转换为异步状态机,即由特殊类型封装/包装。从异步状态机抛出的任何异常都将在任务被执行时被捕获并重新抛出await(除了那些任务async void)或者它们未被观察到,这可以在TaskScheduler.UnobservedTaskException事件中被捕获。
如果async从NonAwaitedMethod方法中删除关键字,则可以捕获异常。
观察这种行为的一个好方法是使用这个:
try
{
NonAwaitedMethod();
// You will still see this message in your console despite exception
// being thrown from the above method synchronously, because the method
// has been encapsulated into an async state machine by compiler.
Console.WriteLine("Method Called");
}
catch (Exception e)
{
Console.WriteLine("Exception Caught");
}
Run Code Online (Sandbox Code Playgroud)
因此,您的代码的编译方式与此类似:
try
{
var stateMachine = new AsyncStateMachine(() =>
{
try
{
NonAwaitedMethod();
}
catch (Exception ex)
{
stateMachine.Exception = ex;
}
});
// This does not throw exception
stateMachine.Run();
}
catch (Exception e)
{
Console.WriteLine("Exception Caught");
}
Run Code Online (Sandbox Code Playgroud)
为什么从 Task 交换到 void 返回类型会导致异常被捕获
如果该方法返回 a Task,则任务会捕获异常。
如果方法是void,则从任意线程池线程重新抛出异常。从线程池线程抛出的任何未处理的异常都会导致应用程序崩溃,因此调试器(或 JIT 调试器)可能正在监视此类异常。
如果您想触发并忘记但正确处理异常,则可以使用ContinueWith为任务创建延续:
NonAwaitedMethod()
.ContinueWith(task => task.Exception, TaskContinuationOptions.OnlyOnFaulted);
Run Code Online (Sandbox Code Playgroud)
注意你必须访问task.Exception属性才能观察到异常,否则任务调度程序仍然会收到UnobservedTaskException事件。
或者,如果需要在 中捕获和处理异常Main,正确的方法是使用异步 Main 方法。
如果在该“同步代码”期间抛出异常,为什么不传播到调用方法?(就像普通的同步方法一样。)
好问题。而事实上,早期预览版async/await 也有这种行为。但是语言团队认为这种行为太令人困惑了。
当你有这样的代码时,很容易理解:
if (test)
throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));
Run Code Online (Sandbox Code Playgroud)
但是这样的代码呢:
await Task.Delay(1);
if (test)
throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));
Run Code Online (Sandbox Code Playgroud)
请记住,如果其等待已完成,则await 同步执行。那么在Task.Delay等待任务返回时已经过去了 1 毫秒吗?或者更现实的例子,当HttpClient返回本地缓存的响应(同步)时会发生什么?更一般地,在方法的同步部分直接抛出异常往往会导致代码根据竞争条件改变其语义。
因此,决定单方面改变所有async方法的工作方式,以便所有抛出的异常都放在返回的任务上。作为一个很好的副作用,这使它们的语义与枚举器块保持一致;如果您有一个使用 的方法,yield return则在实现枚举器之前不会看到任何异常,而不是在调用该方法时。
关于你的场景:
Main通过忽略任务来执行“即发即忘”。“即发即弃”的意思是“我不在乎异常”。如果您确实关心异常,那么不要使用“即发即忘”;相反,await任务在某个时候。任务是async方法如何向调用者报告它们的完成情况,而执行任务是await调用代码如何检索任务的结果(并观察异常)。async void是一个奇怪的怪癖(一般应该避免)。它被放入语言中以支持异步事件处理程序,因此它具有类似于事件处理程序的语义。具体来说,任何逃脱该async void方法的异常都会在该方法开始时当前的顶级上下文中引发。这就是异常也适用于 UI 事件处理程序的方式。在控制台应用程序的情况下,异常会在线程池线程上引发。普通async方法返回一个“句柄”,表示异步操作并可以保存异常。async void无法捕获方法的异常,因为这些方法没有“句柄”。附带说明一下,永远不要使用Task构造函数。如果要在线程池上运行代码,请使用Task.Run. 如果您想要异步委托类型,请使用Func<Task>.
| 归档时间: |
|
| 查看次数: |
2601 次 |
| 最近记录: |