为什么构造函数中的异步操作会吞噬异常?
Why is an exception swallowed in an async action in a constructor?
我认为我很聪明地找到了一种在 ctor 中调用 ync 方法的方法:
public AppStateModel(IBranchClient branchClient)
{
_branchClient = branchClient;
var loadBranch = new Action(async () =>
{
DataProviderReadResult<BranchDetailViewModel> result = await _branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);
});
loadBranch();
}
但是动作的主体抛出一个异常,我用普通 throw;
记录并重新抛出,但是这个 ctor 执行得很好,我的代码的其余部分继续 运行 好像什么都没有发生。这是为什么?
因为动作是异步的。 loadBranch()
returns 一旦到达第一个 await
,并且不可能抛出您期望的异常 - 这是 Task
中信息的一部分你忽略了(通过使用 Action
而不是 Func<Task>
)。
总而言之,您刚刚编写了一个更加混淆的版本:
_branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);
.NET 构造函数本质上是同步的。他们不应该做任何你可以从使用异步代码中获益的事情 - 在构造函数(或构造函数调用的方法)中尽可能少做是个好主意。如果您需要复杂的操作、异步代码、I/O、大量 CPU 工作...请使用静态方法。由于您是 运行 异步代码,因此请将其 return 设为 Task<AppStateModel>
,以适合整个异步流程。
另请注意,该异常将不会 在较旧的 .NET 运行时中被吞没。假设没有同步上下文,异常仍然在后台线程上抛出(异步操作的延续被发布)——thread-pool 线程上未处理异常的默认值曾经是 "bring down the whole application"。这会在 Task
对象被最终确定时发生,因此与所有程序逻辑分离,据您所知几乎是随机的。毕竟,您还能做什么 - 没有可以观察到异常的好地方,而且那里唯一的根基本上是 "I don't care about what happens with this task"。但是考虑到确保正确观察和处理每个异常是多么复杂,默认值更改为 "unobserved exceptions are ignored".
让我们解构这里发生的事情:您有一个指向匿名 async void
方法的 Action 委托。 async void
是什么意思?这意味着该方法实际上 returns 一个 Task
封装了异步逻辑。
意思是当你调用loadBranch
时,它执行了一个异步方法。当异步方法命中 await
调用时,它会 returns 一个 Task
对象,允许您等待它、添加一个延续或其他任何东西。但是由于您没有明确的 Task 变量来捕获它,您只是让构造函数超出范围,没有任何代码处理 Task 的继续。这意味着当 Task 抛出时,ctor 已经退出。
我认为我很聪明地找到了一种在 ctor 中调用 ync 方法的方法:
public AppStateModel(IBranchClient branchClient)
{
_branchClient = branchClient;
var loadBranch = new Action(async () =>
{
DataProviderReadResult<BranchDetailViewModel> result = await _branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);
});
loadBranch();
}
但是动作的主体抛出一个异常,我用普通 throw;
记录并重新抛出,但是这个 ctor 执行得很好,我的代码的其余部分继续 运行 好像什么都没有发生。这是为什么?
因为动作是异步的。 loadBranch()
returns 一旦到达第一个 await
,并且不可能抛出您期望的异常 - 这是 Task
中信息的一部分你忽略了(通过使用 Action
而不是 Func<Task>
)。
总而言之,您刚刚编写了一个更加混淆的版本:
_branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);
.NET 构造函数本质上是同步的。他们不应该做任何你可以从使用异步代码中获益的事情 - 在构造函数(或构造函数调用的方法)中尽可能少做是个好主意。如果您需要复杂的操作、异步代码、I/O、大量 CPU 工作...请使用静态方法。由于您是 运行 异步代码,因此请将其 return 设为 Task<AppStateModel>
,以适合整个异步流程。
另请注意,该异常将不会 在较旧的 .NET 运行时中被吞没。假设没有同步上下文,异常仍然在后台线程上抛出(异步操作的延续被发布)——thread-pool 线程上未处理异常的默认值曾经是 "bring down the whole application"。这会在 Task
对象被最终确定时发生,因此与所有程序逻辑分离,据您所知几乎是随机的。毕竟,您还能做什么 - 没有可以观察到异常的好地方,而且那里唯一的根基本上是 "I don't care about what happens with this task"。但是考虑到确保正确观察和处理每个异常是多么复杂,默认值更改为 "unobserved exceptions are ignored".
让我们解构这里发生的事情:您有一个指向匿名 async void
方法的 Action 委托。 async void
是什么意思?这意味着该方法实际上 returns 一个 Task
封装了异步逻辑。
意思是当你调用loadBranch
时,它执行了一个异步方法。当异步方法命中 await
调用时,它会 returns 一个 Task
对象,允许您等待它、添加一个延续或其他任何东西。但是由于您没有明确的 Task 变量来捕获它,您只是让构造函数超出范围,没有任何代码处理 Task 的继续。这意味着当 Task 抛出时,ctor 已经退出。