C# Async Tips and Tricks Part 2 : Async Void

C# Async Tips and Tricks Part 2 : Async Void

TL;DR: This article discusses the differences between async Task and async void, and how async void methods and async void lambdas, used outside the DispatcherSynchronizationContext, can crash the process if exceptions are not handled.

You can also read the part 3, Tasks and the Synchronization Context.

In the first part of the series, we discussed the behavior of async methods. The second part discusses how async Task and async void methods can differ in behavior, while seemingly being similar.

 

www.jaylee.org/post/2012/06/18/CSharp-5-0-Async-Tips-and-Tricks-Part-1.aspx  这个链接废了,要用下面的链接

https://jaylee.org/archive/2012/06/19/csharp-5-0-async-tips-and-tricks-part-1.html

https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.html

jaylee.org/post/2012/09/29/C-Async-Tips-and-Tricks-Part-3-Tasks-and-the-Synchronization-Context.aspx 这个链接也废了,要用下面的链接

https://jaylee.org/archive/2012/09/30/c-async-tips-and-tricks-part-3-tasks-and-the-synchronization-context.html

 

Authoring Async Methods

Async methods can be authored in three different ways:

1
async Task MyMethod() { }

which creates a method that can be awaited, but does not return any value,

 

1
async Task MyReturningMethod {     return default (T); }

which creates a method that can be awaited, and returns a value of the type T,

 

1
async     void MyFireAndForgetMethod() { }

which is allows for fire and forget methods, and cannot be awaited.

You may be wondering why there are two ways to declare a void returning method. Read on.

[more]

async void methods

async void methods exist for the single purpose of being used as an event handler. More specifically, it has been introduced to support UI elements event handlers, nothing else.

It allows writing simple async methods such as this one:

1
private async     void Button1_Click(    object sender, EventArgs args) { }

All Microsoft provided events respect a BCL guideline for which all .NET event delegates return void, which is in part why the async Task declaration cannot not be used.

An async void method is handled differently under the hood by the compiler. Task returning async methods are managed by the AsyncTaskMethodBuilder class, whereas async void methods are managed by the AsyncVoidMethodBuilder class.  从底层来说async Task和async void是由不同的MethodBuilder来管理的

Both builders handle the synchronization context capture the same way, where the ambient周围的 context is reused to execute each block of code between the awaits of an async method.

This means than in normal circumstances, the code executes the same way in both Task and void returning methods.

But there is one fundamental difference between async Task and async void: Exceptions handling.

 

async Task methods and Exceptions

Let’s consider this example, using an async Task:

1
2
3
4
5
6
7
8
9
10
static void Main(    string [] args)
{
   Test();
   Console.ReadLine();
}
 
public static async Task Test()
{
   throw new Exception();
}

When executed, this program will not end until a key is pressed. The exception will be stored in an anonymous Task instance, because the return value of call to Test is never awaited, nor stored in a variable.

It is a pretty bad practice to write this kind of code, because the exception is essentially实质上 silent, and may hide a bug. The compiler is a bit helpful in that regard to avoid writing that kind of code, and will provide you with the following warning message:

warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

As a best practice, you should never have this warning anywhere in your code, and handle exceptions properly.

 

TPL unhandled exceptions

The TPL, which used behind async methods, handles exceptions differently than the ThreadPool, and unobserved exceptions will be thrown in the UnobservedTaskException event.

If we modify a bit our sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void Main(    string [] args)
{
    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    Test();
 
    Console.ReadLine();
 
    GC.Collect();
    GC.WaitForPendingFinalizers();
 
    Console.ReadLine();
}
 
static void TaskScheduler_UnobservedTaskException(    object sender, UnobservedTaskExceptionEventArgs e)
{
    Console.WriteLine(e.Exception);
}
 
public static async Task Test()
{
    throw new Exception();
}

We’ll notice that the event is not raised immediately after the task is in a faulted state. The task instance must be collected before, which is why forcing a garbage collection will raise the event.

As a best practice, you should always add a handler to the UnobservedTaskException as a safety net to log those exceptions, and probably treat them as potential bugs.

 

 

 

 

async void methods and Exceptions

Now we’ll be using the same code, but change the async Task to an async void method:

1
2
3
4
5
6
7
8
9
10
static void Main(    string [] args)
{
    Test();
    Console.ReadLine();
}
 
public static async     void Test()
{
    throw new Exception();
}

First, you’ll notice that the compiler warning that was present with async Task is no longer shown.

Second, when run, the whole process crashes.

The reason behind this is the Synchronization Context used by the AsyncVoidMethodBuilder, being none in this example. When there is no ambient Synchronization Context, any exception that is unhandled by the body of an async void method is rethrown on the ThreadPool.

While there is seemingly no other logical place where that kind of unhandled exception could be thrown, the unfortunate effect is that the process is being terminated, because unhandled exceptions on the ThreadPool effectively terminate the process since .NET 2.0. You may intercept拦截 all unhandled exception using the AppDomain.UnhandledException event, but there is no way to recover the process from this event.

To make the matter a bit more difficult, this event is not available in Metro apps, because the AppDomain class has been removed.

 

Async lambdas

Unless you consider yourself as reliable enough to never forget to place a try catch block in all async void methods, you should never use an async void method anywhere but in UI event handlers, such as behind button clicks and alike.

You could make a code review and search for all async void methods using a find and replace... but you won’t find this one:

1
new List<    int >().ForEach(async i => {     throw new Exception(); });

The method signature for List<T>.ForEach is :

1
public void ForEach(Action action);

for which it is valid to provide and async void lambda, putting the process at risk of termination.

This kind of code is very easy to write when moving stateful code to async-only APIs, and you may crash your process without being warned that you’re writing dangerous code.

An other interesting fact about these async void lambdas is that the compiler, when facing these two overloads :

1
2
public void ForEach(Action action);
public void ForEach(Func<t, task=""> action);</t,>

with this call :

1
ForEach(async i => { });

Will automatically choose the Task returning Func<>.

 

async void is for UI Event Handlers Only

When writing UI event handlers, async void methods are somehow painless because exceptions are treated the same way found in non-async methods; they are thrown on the Dispatcher. There is a possibility to recover from such exceptions, with is more than correct for most cases.

Outside of UI event handlers however, async void methods are somehow dangerous to use and may not that easy to find.

This is why I’ve been using a custom Static Analysis (fxcop) rule that prevents the use of the AsyncVoidMethodBuilder class in my apps, effectively banning both explicit and implicit async void methods.

Most of this code is using an MVVM approach where ICommand data-binding is used, therefore limiting the use of any async void methods.

 

FxCop provides a tool to help developers to follow their company's coding standards. FxCop does code analysis to check whether the new code is compliant with the coding standards and naming conventions followed by the company. FxCop will ensure that the specified rules are used in the source code.

 

What to Take Away

Here is what to remember about async Task/void methods:

  • async void methods are only suited for UI event handlers
  • async void methods outside the dispatcher raise unhandled exceptions on the thread pool, which terminate the process
  • async void lambdas are difficult to find, but easy to write. You should be looking for any use of the AsyncVoidMethodBuilder in your generated IL
  • You should always be using or awaiting the return value of an async method (treat CS4014 as an error)
  • Always add a handler to UnobservedTaskException event, to log exceptions in case the previous point is not respected.

I’ll probably soon be publishing a simple code source for the custom static analysis rule that prevents the use AsyncVoidMethodBuilder class, to streamline the process of banning async void methods. 

 

扩展阅读

Frequently asked questions about legacy FxCop and .NET analyzers

 

 

作者:Chuck Lu    GitHub    
posted @   ChuckLu  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2020-07-03 NLog rolling file
2020-07-03 Custom date and time format strings
2020-07-03 NLog layout map to conversionPattern with log4net
2019-07-03 Javascript 面向对象编程(一):封装
2019-07-03 Javascript继承机制的设计思想
2019-07-03 __proto__ VS. prototype in JavaScript
2019-07-03 ClientScriptManager.RegisterClientScriptBlock Method 无效
点击右上角即可分享
微信分享提示