`async void`(没有等待)vs`void`之间有什么区别?

cod*_*452 5 .net c# async-await

摘自Stephen Cleary关于异步等待的文章:

图2异步无效方法的异常无法通过Catch捕获

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}
Run Code Online (Sandbox Code Playgroud)

...异步void方法抛出的任何异常将直接在异步void方法启动时处于活动状态的SynchronizationContext上引发...

这究竟意味着什么?我写了一个扩展示例来尝试收集更多信息.它具有与图2相同的行为:

static void Main()
{

    AppDomain.CurrentDomain.UnhandledException += (sender, ex) => 
    {
        LogCurrentSynchronizationContext("AppDomain.CurrentDomain.UnhandledException");
        LogException("AppDomain.CurrentDomain.UnhandledException", ex.ExceptionObject as Exception);
    };

    try
    {
        try
        {
            void ThrowExceptionVoid() => throw new Exception("ThrowExceptionVoid");

            ThrowExceptionVoid();
        }
        catch (Exception ex)
        {
            LogException("AsyncMain - Catch - ThrowExceptionVoid", ex);
        }

        try
        {
            // CS1998 C# This async method lacks 'await' operators and will run synchronously. 
            async void ThrowExceptionAsyncVoid() => throw new Exception("ThrowExceptionAsyncVoid");

            ThrowExceptionAsyncVoid();
        }
        // exception cannot be caught, despite the code running synchronously.
        catch (Exception ex) 
        {
            LogException("AsyncMain - Catch - ThrowExceptionAsyncVoid", ex);
        }
    }
    catch (Exception ex)
    {
        LogException("Main", ex);
    }

    Console.ReadKey();
}

private static void LogCurrentSynchronizationContext(string prefix)
    => Debug.WriteLine($"{prefix} - " +
        $"CurrentSynchronizationContext: {SynchronizationContext.Current?.GetType().Name} " +
        $"- {SynchronizationContext.Current?.GetHashCode()}");

private static void LogException(string prefix, Exception ex)
    => Debug.WriteLine($"{prefix} - Exception - {ex.Message}");
Run Code Online (Sandbox Code Playgroud)

调试输出:

Exception thrown: 'System.Exception' in ConsoleApp3.dll
AsyncMain - Catch - ThrowExceptionVoid - Exception - ThrowExceptionVoid
Exception thrown: 'System.Exception' in ConsoleApp3.dll
An exception of type 'System.Exception' occurred in ConsoleApp3.dll but was not handled in user code
ThrowExceptionAsyncVoid
AppDomain.CurrentDomain.UnhandledException - CurrentSynchronizationContext:  - 
AppDomain.CurrentDomain.UnhandledException - Exception - ThrowExceptionAsyncVoid
The thread 0x1c70 has exited with code 0 (0x0).
An unhandled exception of type 'System.Exception' occurred in System.Private.CoreLib.ni.dll
ThrowExceptionAsyncVoid
The program '[18584] dotnet.exe' has exited with code 0 (0x0).
Run Code Online (Sandbox Code Playgroud)

我想要更多细节

  • 如果没有当前的同步上下文(如我的示例所示),引发的异常在哪里?
  • async void(没有await)和.之间有什么区别?void
    • 编译器发出警告 CS1998 C# This async method lacks 'await' operators and will run synchronously.
    • 如果它与no同步运行await,为什么它与简单的行为不同void
    • 难道async Taskawait也是从不同的表现Task
  • 编译器行为与async void和之间有什么不同async Task.是否Task真的按照这里的async void建议创建了一个对象?

编辑.需要明确的是,这不是关于最佳实践的问题 - 它是关于编译器/运行时实现的问题.

Ste*_*ary 6

如果没有当前的同步上下文(如我的示例所示),引发的异常在哪里?

按照惯例,当SynchronizationContext.Currentnull,这真的一样SynchronizationContext.Current等于一个实例new SynchronizationContext().换句话说,"无同步上下文"与"线程池同步上下文"相同.

因此,您看到的行为是async状态机正在捕获异常,然后直接在线程池线程上提升它,在线程池中无法捕获它catch.

这种行为看起来很奇怪,但请考虑这种方式:async void用于事件处理程序.所以考虑一个提升事件的UI应用程序; 如果它是同步的,那么任何异常都会传播到UI消息处理循环.该async void行为旨在模仿:await在UI消息处理循环中重新引发任何异常(包括之后的异常).同样的逻辑应用于线程池上下文; 例如,同步System.Threading.Timer回调处理程序中的异常将直接在线程池上引发,异步System.Threading.Timer回调处理程序中的异常也是如此.

如果它同步运行而没有等待,为什么它的行为与简单的无效?

async状态机是专门处理该异常.

没有等待的异步任务的行为与任务有什么不同吗?

绝对.async Task有一个非常相似的状态机 - 它捕获代码中的任何异常并将它们放在返回的代码上Task.这是删除async/ await使用非平凡代码的陷阱之一.

async void和async Task之间的编译器行为有何不同?

对于编译器,区别在于如何处理异常.

思考这个问题的正确方法async Task是自然而恰当的语言发展; async void是一个奇怪的黑客,C#/ VB团队采用它来启用异步事件而没有巨大的向后兼容性问题.其他async/ await启用的语言,如F#,Python和JavaScript,没有async void......的概念,从而避免了所有的陷阱.

是否真的在这里建议的async void创建了一个Task对象?

没有.