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

What are the differences between `async void` (with no await) vs `void`

取自 article Stephen Cleary 的异步等待:

图 2 无法使用 Catch 捕获 Async Void 方法的异常

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

... any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started...

这到底是什么意思?我写了一个扩展示例来尝试收集更多信息。它具有与图 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}");

调试输出:

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).

我想要更多详情

编辑。 明确地说,这不是关于最佳实践的问题 - 这是关于编译器/运行时实现的问题。

  1. async void - 它不能被等待,它允许你触发或忘记方法
  2. 异步任务 - 可以等待,但 return 没有任何值
  3. 异步任务方法名{ return默认(T); } - 可以等待,returns 是 T
  4. 类型的值
  5. void - 没有参数将被 returned

If there is no current synchronization context (as in my example), where is the exception raised?

By convention,当SynchronizationContext.Currentnull时,这实际上与SynchronizationContext.Current等于new SynchronizationContext()的一个实例是一样的。换句话说,"no synchronization context" 与 "thread pool synchronization context".

相同

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

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

If it runs synchronously with no await, why is it behaving differently from simply void?

async 状态机正在专门处理异常。

Does async Task with no await also behave differently from Task?

当然可以。 async Task 有一个非常相似的状态机——它从您的代码中捕获任何异常并将它们放在返回的 Task 上。这是 one of the pitfalls in eliding async/await for non-trivial code.

What are the differences in compiler behaviour between async void and async Task.

对于编译器而言,不同之处在于如何处理异常。

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

Is a Task object really created under-the-hood for async void as suggested here?

没有