未使用 await 调用时在异步方法中捕获异常
Catching Exceptions in async methods when not called with await
目标:
我对在 .Net Core 库中看到的异常行为感到困惑。这个问题的目的是了解为什么它正在做我所看到的。
执行摘要
我以为调用一个async
方法的时候,里面的代码是同步执行的,直到命中第一个await。如果是这种情况,那么,如果在 "synchronous code" 期间抛出异常,为什么它没有传播到调用方法? (就像普通的同步方法一样。)
示例代码:
在 .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);
}
}
场景:
当运行原样时,这段代码会抛出一个异常,但是,除非你在上面设置了一个断点,否则你是不会知道的。 Visual Studio 调试器和控制台都不会给出任何存在问题的指示(除了输出屏幕中的一行注释)。
将 NonAwaitedMethod
的 return 类型从 Task
替换为 void
。这将导致 Visual Studio 调试器现在在出现异常时中断。它还将在控制台中打印出来。但值得注意的是,异常是 NOT 在 Main
.
中发现的 catch
语句中
将NonAwaitedMethod
的return类型保留为void
,但去掉async
。还要将最后一行从 await runTask;
更改为 runTask.Wait();
(这实际上删除了所有异步内容。)当 运行 时,异常在 catch
语句中的 [=17] 中被捕获=] 方法.
所以,总结一下:
| Scenario | Caught By Debugger | Caught by Catch |
|------------|--------------------|-----------------|
| async Task | No | No |
| async void | Yes | No |
| void | N/A | Yes |
问题:
我认为因为 异常是在 await
完成之前抛出的 ,所以 它会同步执行 直到,并通过抛出异常。
因此我的问题是:为什么场景 1 或场景 2 都没有被 catch
语句捕获?
此外,为什么从 Task
到 void
return 类型交换会导致异常被调试器捕获? (即使我没有使用那种 return 类型。)
exception was thrown before an await was done, that it would execute synchronously
我认为这是相当正确的,但这并不意味着您可以捕捉到异常。
因为您的代码有 async
关键字,它将方法变成异步状态机,即由特殊类型封装/包装。当任务被 await
ed 时(除了那些 async void
的任务)或者它们未被观察到,可以在 TaskScheduler.UnobservedTaskException
中捕获从异步状态机抛出的任何异常都将被捕获并重新抛出事件。
如果从 NonAwaitedMethod
方法中删除 async
关键字,则可以捕获异常。
观察这种行为的一个好方法是使用这个:
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");
}
因此您的代码编译方式与此类似:
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");
}
why does swapping from Task to void return type cause the exception to get caught
如果方法returns一个Task
,异常被任务捕获。
如果方法是 void
,则异常会从任意线程池线程中重新抛出。从线程池线程抛出的任何未处理的异常都会导致应用程序崩溃,因此调试器(或者可能是 JIT 调试器)很可能正在监视此类异常。
如果您想触发并忘记但正确处理异常,您可以使用 ContinueWith
为任务创建一个延续:
NonAwaitedMethod()
.ContinueWith(task => task.Exception, TaskContinuationOptions.OnlyOnFaulted);
注意你必须访问 task.Exception
属性 才能观察到异常,否则任务调度程序仍会收到 UnobservedTaskException
事件。
或者如果需要在 Main
中捕获和处理异常,正确的方法是使用 async Main methods.
if an exception is thrown during that "synchronous code", why is it not propagated up to the calling method? (As a normal synchronous method would do.)
好问题。事实上,async
/await
did 的早期预览版有这种行为。但是语言团队认为这种行为太令人困惑了。
当你有这样的代码时,很容易理解:
if (test)
throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));
但是像这样的代码呢:
await Task.Delay(1);
if (test)
throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));
请记住 await
同步执行 如果其等待已完成。那么,等待从 Task.Delay
编辑的任务 return 时是否已经过了 1 毫秒?或者举一个更现实的例子,当 HttpClient
return 是本地缓存的响应(同步)时会发生什么?更一般地说,在方法的同步部分直接抛出异常往往会导致代码根据竞争条件更改其语义。
因此,决定单方面更改所有 async
方法的工作方式,以便 所有 抛出的异常都放在 returned 任务中.作为一个很好的副作用,这使它们的语义与枚举器块一致;如果您有一个使用 yield return
的方法,则在 实现 之前不会看到任何异常,而不是在 调用 方法时].
关于您的场景:
- 是的,异常被忽略了。因为
Main
中的代码正在通过忽略任务来执行 "fire and forget"。 "fire and forget" 表示 "I don't care about exceptions"。如果您确实关心异常,那么请不要使用"fire and forget";相反,await
某个时候的任务。 The task is how async
methods report their completion to their callers, and doing an await
is how calling code retrieves the results of the task (and observe exceptions).
- 是的,
async void
是一个奇怪的怪癖(和 should be avoided in general)。它被放入支持异步事件处理程序的语言中,因此它具有类似于事件处理程序的语义。具体来说,任何逃避 async void
方法的异常都会在方法开始时的当前顶级上下文中引发。这就是异常也适用于 UI 事件处理程序的方式。对于控制台应用程序,线程池线程会引发异常。正常 async
方法 return a "handle" 表示异步操作并可以容纳异常。无法捕获 async void
方法的异常,因为这些方法没有 "handle"。
- 当然可以。在这种情况下,该方法是同步的,异常会像正常情况一样在堆栈中传播。
旁注,never, ever use the Task
constructor。如果要运行 线程池上的代码,请使用Task.Run
。如果你想要一个异步委托类型,使用 Func<Task>
.
async
关键字表示编译器应该将方法转换为异步状态机,这在异常处理方面是不可配置的。如果要立即抛出 NonAwaitedMethod
方法的同步部分异常,除了从方法中删除 async
关键字外别无选择。通过将异步部分移动到异步 local function:
中,您可以两全其美
public static Task NonAwaitedMethod()
{
Task startupDone = new Task(() => { });
var runTask = DoStuff(() =>
{
startupDone.Start();
});
var didStartup = startupDone.Wait(1000);
if (!didStartup)
{
throw new ApplicationException("Fail One");
}
return ImlpAsync(); async Task ImlpAsync()
{
await runTask;
};
}
除了使用命名函数,您还可以使用匿名函数:
return ((Func<Task>)(async () =>
{
await runTask;
}))();
目标:
我对在 .Net Core 库中看到的异常行为感到困惑。这个问题的目的是了解为什么它正在做我所看到的。
执行摘要
我以为调用一个async
方法的时候,里面的代码是同步执行的,直到命中第一个await。如果是这种情况,那么,如果在 "synchronous code" 期间抛出异常,为什么它没有传播到调用方法? (就像普通的同步方法一样。)
示例代码:
在 .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);
}
}
场景:
当运行原样时,这段代码会抛出一个异常,但是,除非你在上面设置了一个断点,否则你是不会知道的。 Visual Studio 调试器和控制台都不会给出任何存在问题的指示(除了输出屏幕中的一行注释)。
将
NonAwaitedMethod
的 return 类型从Task
替换为void
。这将导致 Visual Studio 调试器现在在出现异常时中断。它还将在控制台中打印出来。但值得注意的是,异常是 NOT 在Main
. 中发现的 将
NonAwaitedMethod
的return类型保留为void
,但去掉async
。还要将最后一行从await runTask;
更改为runTask.Wait();
(这实际上删除了所有异步内容。)当 运行 时,异常在catch
语句中的 [=17] 中被捕获=] 方法.
catch
语句中
所以,总结一下:
| Scenario | Caught By Debugger | Caught by Catch |
|------------|--------------------|-----------------|
| async Task | No | No |
| async void | Yes | No |
| void | N/A | Yes |
问题:
我认为因为 异常是在 await
完成之前抛出的 ,所以 它会同步执行 直到,并通过抛出异常。
因此我的问题是:为什么场景 1 或场景 2 都没有被 catch
语句捕获?
此外,为什么从 Task
到 void
return 类型交换会导致异常被调试器捕获? (即使我没有使用那种 return 类型。)
exception was thrown before an await was done, that it would execute synchronously
我认为这是相当正确的,但这并不意味着您可以捕捉到异常。
因为您的代码有 async
关键字,它将方法变成异步状态机,即由特殊类型封装/包装。当任务被 await
ed 时(除了那些 async void
的任务)或者它们未被观察到,可以在 TaskScheduler.UnobservedTaskException
中捕获从异步状态机抛出的任何异常都将被捕获并重新抛出事件。
如果从 NonAwaitedMethod
方法中删除 async
关键字,则可以捕获异常。
观察这种行为的一个好方法是使用这个:
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");
}
因此您的代码编译方式与此类似:
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");
}
why does swapping from Task to void return type cause the exception to get caught
如果方法returns一个Task
,异常被任务捕获。
如果方法是 void
,则异常会从任意线程池线程中重新抛出。从线程池线程抛出的任何未处理的异常都会导致应用程序崩溃,因此调试器(或者可能是 JIT 调试器)很可能正在监视此类异常。
如果您想触发并忘记但正确处理异常,您可以使用 ContinueWith
为任务创建一个延续:
NonAwaitedMethod()
.ContinueWith(task => task.Exception, TaskContinuationOptions.OnlyOnFaulted);
注意你必须访问 task.Exception
属性 才能观察到异常,否则任务调度程序仍会收到 UnobservedTaskException
事件。
或者如果需要在 Main
中捕获和处理异常,正确的方法是使用 async Main methods.
if an exception is thrown during that "synchronous code", why is it not propagated up to the calling method? (As a normal synchronous method would do.)
好问题。事实上,async
/await
did 的早期预览版有这种行为。但是语言团队认为这种行为太令人困惑了。
当你有这样的代码时,很容易理解:
if (test)
throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));
但是像这样的代码呢:
await Task.Delay(1);
if (test)
throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));
请记住 await
同步执行 如果其等待已完成。那么,等待从 Task.Delay
编辑的任务 return 时是否已经过了 1 毫秒?或者举一个更现实的例子,当 HttpClient
return 是本地缓存的响应(同步)时会发生什么?更一般地说,在方法的同步部分直接抛出异常往往会导致代码根据竞争条件更改其语义。
因此,决定单方面更改所有 async
方法的工作方式,以便 所有 抛出的异常都放在 returned 任务中.作为一个很好的副作用,这使它们的语义与枚举器块一致;如果您有一个使用 yield return
的方法,则在 实现 之前不会看到任何异常,而不是在 调用 方法时].
关于您的场景:
- 是的,异常被忽略了。因为
Main
中的代码正在通过忽略任务来执行 "fire and forget"。 "fire and forget" 表示 "I don't care about exceptions"。如果您确实关心异常,那么请不要使用"fire and forget";相反,await
某个时候的任务。 The task is howasync
methods report their completion to their callers, and doing anawait
is how calling code retrieves the results of the task (and observe exceptions). - 是的,
async void
是一个奇怪的怪癖(和 should be avoided in general)。它被放入支持异步事件处理程序的语言中,因此它具有类似于事件处理程序的语义。具体来说,任何逃避async void
方法的异常都会在方法开始时的当前顶级上下文中引发。这就是异常也适用于 UI 事件处理程序的方式。对于控制台应用程序,线程池线程会引发异常。正常async
方法 return a "handle" 表示异步操作并可以容纳异常。无法捕获async void
方法的异常,因为这些方法没有 "handle"。 - 当然可以。在这种情况下,该方法是同步的,异常会像正常情况一样在堆栈中传播。
旁注,never, ever use the Task
constructor。如果要运行 线程池上的代码,请使用Task.Run
。如果你想要一个异步委托类型,使用 Func<Task>
.
async
关键字表示编译器应该将方法转换为异步状态机,这在异常处理方面是不可配置的。如果要立即抛出 NonAwaitedMethod
方法的同步部分异常,除了从方法中删除 async
关键字外别无选择。通过将异步部分移动到异步 local function:
public static Task NonAwaitedMethod()
{
Task startupDone = new Task(() => { });
var runTask = DoStuff(() =>
{
startupDone.Start();
});
var didStartup = startupDone.Wait(1000);
if (!didStartup)
{
throw new ApplicationException("Fail One");
}
return ImlpAsync(); async Task ImlpAsync()
{
await runTask;
};
}
除了使用命名函数,您还可以使用匿名函数:
return ((Func<Task>)(async () =>
{
await runTask;
}))();