c# – `async void`(没有等待)和“void`之间有什么区别

截止于article on async等待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;
  }
}

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

我想要更多细节

>如果没有当前的同步上下文(如我的示例所示),引发的异常在哪里?
> async void(没有等待)和void之间有什么区别

>编译器警告CS1998 C#这种异步方法缺少’await’运算符并将同步运行.
>如果它同步运行而没有等待,为什么它的行为与简单的无效?
>没有等待的异步任务的行为与任务有什么不同吗?

> async void和async Task之间的编译器行为有何不同?是否真的根据建议的here创建了async void的Task对象?

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

最佳答案

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

By convention,当SynchronizationContext.Current为null时,它实际上与SynchronizationContext.Current相同,等于新SynchronizationContext()的实例.换句话说,“无同步上下文”与“线程池同步上下文”相同.

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

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

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

异步状态机特别处理异常.

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 void是一个奇怪的黑客,C#/ VB团队采用它来启用异步事件而没有巨大的向后兼容性问题.其他异步/等待启用的语言,如F#,Python和JavaScript,没有async void的概念……因此避免了所有的陷阱.

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

没有.

转载注明原文:c# – `async void`(没有等待)和“void`之间有什么区别 - 代码日志