为什么在构造函数中的异步操作中会吞下异常?

时间:2017-02-06 12:19:42

标签: c# asynchronous constructor async-await anonymous-function

我以为我很聪明地找到了一种在ctor中调用aync方法的方法:

public AppStateModel(IBranchClient branchClient)
{
    _branchClient = branchClient;
    var loadBranch = new Action(async () =>
    {
        DataProviderReadResult<BranchDetailViewModel> result = await _branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);
    });
    loadBranch();
}

但是动作的主体抛出一个异常,我用普通throw;记录并重新抛出,但是这个ctor执行正常,我的其余代码继续运行,好像没有任何反应。这是为什么?

2 个答案:

答案 0 :(得分:7)

因为动作是异步的。 loadBranch()会在到达第一个await后立即返回,并且无法抛出您期望的异常 - 这是Task中您忽略的信息的一部分(使用Action代替Func<Task>)。

总而言之,您刚刚编写了一个更加混淆的版本:

_branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);

.NET构造函数本质上是同步的。它们不应该在您使用异步代码的情况下做任何事情 - 并且在构造函数(或构造函数调用的方法)中尽可能少地做它是个好主意。如果您需要复杂的操作,异步代码,I / O,大量的CPU工作...使用静态方法。因为你正在运行异步代码,所以让它返回Task<AppStateModel>,在整个异步流程中正确拟合。

另请注意,在旧的.NET运行时中,会被吞噬。假设没有同步上下文,异常仍然在后台线程上抛出(其中发布了异步操作的延续) - 并且线程池线程上未处理的异常的默认值曾经是“降低整个应用程序”。当Task对象被最终确定时会发生这种情况,因此与您的所有程序逻辑分离,就您所知,它几乎是随机的。毕竟,你还能做些什么 - 没有可以观察到异常的好地方,那里唯一的根本就是“我不关心这项任务会发生什么”。但考虑到确保正确观察和处理每个异常的复杂程度,默认值已更改为“忽略未观察到的异常”。

答案 1 :(得分:2)

让我们解构这里发生的事情:你有一个指向匿名async void方法的Action委托。 async void是什么意思?这意味着该方法实际上返回一个封装异步逻辑的Task

这意味着当您调用loadBranch时,它会执行异步方法。当异步方法命中await时,它会返回一个Task对象,允许您等待它,添加延续或其他任何内容。但是由于你没有明确的Task变量来捕获它,你只需让你的构造函数超出范围,而没有任何代码处理Task的延续。这意味着当任务抛出时,ctor已经退出。