C# 为什么来自async void的异常会使应用程序崩溃,但来自async任务的异常会被吞没

C# 为什么来自async void的异常会使应用程序崩溃,但来自async任务的异常会被吞没,c#,.net,asynchronous,async-await,C#,.net,Asynchronous,Async Await,我知道异步任务的异常可以通过以下方式捕获: try { await task; } catch { } 而异步void不能,因为它不能等待 但是为什么当异步任务没有等待时(就像异步无效任务一样,异常会被吞没,而无效的任务会使应用程序崩溃 调用方:ex() 调用: async void ex() { throw new Exception(); } async Task ex() { throw new Exception(); } 因为,您的方法不是异步执行的 执行将同步运行,直到“满足”w

我知道
异步任务的异常可以通过以下方式捕获:

try { await task; }
catch { }
异步void
不能,因为它不能等待

但是为什么当异步任务没有等待时(就像异步无效任务一样,
异常
会被吞没,而无效的任务会使应用程序崩溃

调用方:
ex()

调用

async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }

因为,您的方法不是异步执行的

执行将同步运行,直到“满足”
wait
关键字

所以在
void
的情况下,应用程序将抛出异常,因为异常发生在当前执行上下文中

任务
的情况下,即使同步抛出异常,它也将被包装在
任务
中并返回给调用方

如果要在函数中使用
wait
,则应使用
void
获得所需的行为

async void Ex()
{
    await Task.Delay(1000);
    throw new Exception();
}
TL;博士 这是因为不应该使用
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(异常)语句。对于返回
async
方法的
Task
,这将是一个
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;
}
AsyncMethodBuilderCore.ThrowAsync
helper中的逻辑决定:

  • 如果存在
    SynchronizationContext
    (例如,我们在WPF应用程序的UI线程上),异常将发布在该上下文上
  • 否则,异常将在
    ThreadPool
    线程上排队
在这两种情况下,异常都不会被围绕
ex()
调用设置的
try-catch
块捕获(除非您有一个特殊的
SynchronizationContext
可以执行此操作,请参阅例如Stephen Cleary的)


原因很简单:当我们posta
throw
操作或enqueue它时,我们只需从
ex()
方法返回,从而离开
try catch
块。然后,执行已发布/排队的操作(在同一线程上或在不同线程上)。

请阅读底部的重要说明。

async void
方法将使应用程序崩溃,因为没有
Task
对象供C#编译器将异常推入。在功能级别上,
任务
-返回方法上的
async
关键字只是一种沉重的语法糖分,它告诉编译器使用对象上可用的各种方法以及实用程序(如
任务.FromResult
任务.FromException
)重写
任务中的方法,和
Task.FromCancelled
,或者有时是
Task.Run
,或者从编译器的角度来看是等效的。这意味着类似以下代码:

async Task Except()
{
    throw new Exception { };
}
变成大约:

因此,当您调用
任务
——返回
抛出
异步
方法时,程序不会崩溃,因为实际上没有抛出异常;相反,将在“excepted”状态下创建
任务
对象,并将其返回给调用者。如前所述,
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.
}
Task Except()//请注意,不存在异步修饰符。
{
抛出新异常{};//无论发生什么,现在都将抛出新异常。
return Task.FromResult(0);//Task派生自Task,因此这是一个隐式强制转换。
}
等待调用实际上会抛出
任务
返回
async
方法中假定存在的异常,原因是
wait
关键字被设计为抛出被吞没的
异常,以便在异步上下文中更容易调试

重要提示
编译器实际处理这些“重写”的方式以及编译后的代码所体现的方式可能不同于我所暗示的方式,但在功能级别上大致相同。

如果应用程序无效,则会抛出异常,因为异常发生在当前执行上下文中
-所以为什么在try-catch块(在调用者中)中包装时它没有被捕获?
等待任务。延迟(1000)-不。仍然会使应用程序崩溃。当void的应用程序崩溃时,void应该引用异常答案Alex。
Task Except()
{
    return Task.FromException(new Exception { });
}
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.
}