我了解到async Task
的异常可以被捕获:
try { await task; }
catch { }
async void
之所以不能,是因为无法等待。
但是为什么不等待异步 Task (就像异步 void 一样)却吞下Exception
,而 > void 导致应用程序崩溃?
呼叫者:ex();
已致电:
async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }
答案 0 :(得分:13)
这是因为不应该使用async void
! async void
只能使旧代码起作用(例如WindowsForms和WPF中的事件处理程序)。
这是因为C#编译器如何为async
方法生成代码。
您应该知道,async
/ await
的后面是编译器生成的状态机(IAsyncStateMachine
实现)。
在声明async
方法时,将为其生成状态机struct
。对于您的ex()
方法,此状态机代码如下所示:
void IAsyncStateMachine.MoveNext()
{
try
{
throw new Exception();
}
catch (Exception exception)
{
this.state = -2;
this.builder.SetException(exception);
}
}
请注意,this.builder.SetException(exception);
语句。对于返回Task
的{{1}}方法,这将是一个async
对象。对于AsyncTaskMethodBuilder
方法,它将是void ex()
。
AsyncVoidMethodBuilder
方法主体将由编译器替换为以下内容:
ex()
(对于private static Task ex()
{
ExAsyncStateMachine exasm;
exasm.builder = AsyncTaskMethodBuilder.Create();
exasm.state = -1;
exasm.builder.Start<ExAsyncStateMachine>(ref exasm);
return exasm.builder.Task;
}
,将没有最后async void ex()
行)
方法构建器的return
方法将调用状态机的Start<T>
方法。状态机的方法在其MoveNext
块中捕获异常。通常应在catch
对象上观察到此异常-Task
方法将该异常对象存储在AsyncTaskMethodBuilder.SetException
实例中。当我们删除那个Task
实例(没有Task
)时,我们根本看不到异常,但是该异常本身不再抛出。
在await
的状态机中,有一个async void ex()
。它的AsyncVoidMethodBuilder
方法看起来有所不同:由于没有SetException
存储异常的位置,因此必须抛出该异常。但是,它发生的方式不同,而不仅仅是普通的Task
:
throw
AsyncMethodBuilderCore.ThrowAsync(exception, synchronizationContext);
助手决定的内部逻辑:
AsyncMethodBuilderCore.ThrowAsync
(例如,我们在WPF应用程序的UI线程上),则会在该上下文中发布该异常。SynchronizationContext
线程中排队。在两种情况下,都不会在ThreadPool
调用周围设置的try-catch
块捕获异常(除非您有一个特殊的ex()
可以执行此操作) ,请参见例如Stephen Cleary的AsyncContext
)。
原因很简单:当我们发布一个SynchronizationContext
动作或排队它时,我们只需从throw
方法返回,从而离开ex()
块。然后,执行发布/排队操作(在相同或不同线程上)。
答案 1 :(得分:1)
因为,您的方法不是异步执行的。
执行将同步运行,直到它“满足” await
关键字为止。
因此,在void
的情况下,应用程序将引发异常,因为异常发生在当前执行上下文中。
在Task
甚至同步引发异常的情况下,它也会包装在Task
中并返回给调用方。
如果您将在函数中使用void
,则await
也应获得预期的行为。
async void Ex()
{
await Task.Delay(1000);
throw new Exception();
}
答案 2 :(得分:1)
请阅读底部的重要说明。
async void
方法将使应用程序崩溃,因为C#编译器没有将Task
对象推入异常。从功能上讲,返回async
的方法上的Task
关键字只是繁重的语法糖,它告诉编译器使用各种可用的方法根据Task
对象来重写您的方法从对象上来看,以及诸如Task.FromResult
,Task.FromException
和Task.FromCancelled
之类的实用程序,有时甚至是Task.Run
,或从编译器的角度来看的等效项。这意味着代码如下:
async Task Except()
{
throw new Exception { };
}
大约变成了 :
Task Except()
{
return Task.FromException(new Exception { });
}
,因此,当您调用Task
的{{1}}-返回async
方法时,该程序不会崩溃,因为实际上没有引发异常。而是以“例外”状态创建throw
对象,并将其返回给调用方。如前所述,装饰了Task
的方法没有要返回的async void
对象,因此编译器不会尝试使用Task
对象来重写该方法,而是相反,它只会尝试获取等待的呼叫的值。
Task
的返回方法实际上也可能导致异常,即使由于Task
引起吞咽的原因而没有等待时,也是如此,因此,如果不存在,则方法中的异常也不会被吞下,如下所示。
async
等待呼叫实际上会Task Except() // Take note that there is no async modifier present.
{
throw new Exception { }; // This will now throw no matter what.
return Task.FromResult(0); // Task<T> derives from Task so this is an implicit cast.
}
返回throw
方法中抛出的异常实际上是Task
的原因,是因为async
关键字应该被吞下通过设计await
使异步环境中的调试更加容易。
这些“重写”由编译器实际处理并由编译后的代码表示的方式可能与我所暗示的方式不同,但在功能级别上大致相同。