Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Taken from article on async await by Stephen Cleary:

Figure 2 Exceptions from an Async Void Method Can’t Be Caught with 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...

What does that actually mean? I wrote an extended example to try and glean more info. It has the same behaviour as Figure 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}");

Debug output:

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

I want more details

  • If there is no current synchronization context (as in my example), where is the exception raised?
  • What are the differences between async void (with no await) and void
    • The compiler warns CS1998 C# This async method lacks 'await' operators and will run synchronously.
    • 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?
  • What are the differences in compiler behaviour between async void and async Task. Is a Task object really created under-the-hood for async void as suggested here?

Edit. To be clear, this isn't a question about best practices - it is a question about compiler / runtime implementation.

like image 639
coder958452 Avatar asked Jul 22 '17 09:07

coder958452


1 Answers

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

By convention, when SynchronizationContext.Current is null, that's really the same as SynchronizationContext.Current equal to an instance of new SynchronizationContext(). In other words, "no synchronization context" is the same as the "thread pool synchronization context".

So, the behavior you're seeing is that the async state machine is catching the exception and then raising it directly on a thread pool thread, where it cannot be caught with catch.

This behavior seems odd, but think about it this way: async void is intended for event handlers. So consider a UI application raising an event; if it is synchronous, then any exceptions get propagated to the UI message processing loop. The async void behavior is intended to mimic that: any exceptions (including ones after await) are re-raised on the UI message processing loop. This same logic is applied to the thread pool context; e.g., exceptions from your synchronous System.Threading.Timer callback handler will be raised directly on the thread pool, and so will exceptions from your asynchronous System.Threading.Timer callback handler.

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

The async state machine is handling the exceptions specially.

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

Absolutely. async Task has a very similar state machine - it catches any exceptions from your code and places them on the returned Task. This is 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.

For the compiler, the difference is just how exceptions are handled.

The proper way to think about this is that async Task is a natural and appropriate language development; async void is a weird hack that the C#/VB team adopted to enable asynchronous events without huge backwards-compatibility issues. Other async/await-enabled languages such as F#, Python, and JavaScript have no concept of async void... and thus avoid all of its pitfalls.

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

No.

like image 72
Stephen Cleary Avatar answered Oct 24 '22 11:10

Stephen Cleary