用C#设计一个好的异步方法:
- 它应该有尽量少的参数,甚至不要参数。如果可能的话一定要避免
ref和out参数。 - 如果有意义的话,他应该有一个返回类型,他能真正的表达方法代码的结果,而不是像 C++ 那种成功标识。
- 它应该有一个可以解释自己行为的命名,而不依赖于额外的符号或注释。
使用Async void是一大禁忌:
Async void 方法中抛出的异常无法通过外面的方法捕获
当 async Task或 async Task<T> 方法中抛出异常,这个异常会被捕获并且放置到 Task 对象中。而 Async void 中没有 Task 对象,所以 Async void 抛出任何异常都将直接在启动异步 void 方法时处于活动状态的 SynchronizationContext 上引发。
SynchronizationContext
SynchronizationContext 可以使一个线程与另一个线程进行通信。假设你有两个线程,Thead1 和 Thread2。Thread1 做某些事情,然后它想在 Thread2 里面执行一些代码。一个可以实现的方式是:请求 Thread 得到 SynchronizationContext 这个对象,把它给 Thread1,然后 Thread1 可以调用 SynchronizationContext 的 send 方法在 Thread2 里面执行代码。
不是每一个线程都有一个 SynchronizationContext 对象。一个总是有 SynchronizationContext 对象的是 UI 线程。UI 线程中的控件被创建的时候会把 SynchronizationContext 对象放到这个线程中。
SynchronizationContext.Current 对象不是一个 AppDomain 一个实例的,而是每个线程一个实例。这就意味着两个线程在调用Synchronization.Current时将会拥有他们自己的 SynchronizationContext 对象实例。Context 上下文存储在线程data store(不是在 appDomain 的全局内存空间)。
SynchronizationContext 中有 send、post 静态方法。
public async void AsyncVoidMethodThrowsException()
{
throw new Exception("Hmmm, something went wrong!");
}
public void ThisWillNotCatchTheException()
{
try
{
AsyncVoidMethodThrowsException();
}
catch(Exception ex)
{
//The below line will never be reached
Debug.WriteLine(ex.Message);
}
}
public async Task AsyncTaskMethodThrowsException()
{
throw new Exception("Hmmm, something went wrong!");
}
public async Task ThisWillCatchTheException()
{
try
{
await AsyncTaskMethodThrowsException();
}
catch (Exception ex)
{
//The below line will actually be reached
Debug.WriteLine(ex.Message);
}
}
Async void方法很难被测试
返回 Task 而不是返回 await
public async Task<string> AsyncTask()
{
//Not great!
//...Non-async stuff happens here
//The await is the very last line of the code path - There is no continuation after it
return await GetData();
}
public Task<string> JustTask()
{
//Better!
//...Non-async stuff happens here
//Return a Task instead
return GetData();
}
因为每次将一个方法声明为 async 时,编译器都会创建一个状态机类来封装这个方法逻辑,这增加了一定量的开销。如果这个方法不需要异步,而是返回一个 Task<T>,让其他合适的地方处理它, 使方法的返回类型为 Task<T> (而不是 async T),这样就避免了状态机的生成,从而使代码更简洁,耗时更少。
但是,凡事都有例外,如果返回一个 Task<T>,那么返回将立即发生,因此,如果代码在 try/catch 块中,则不会捕获异常。类似地,如果代码在 using块中,它将立即释放对象。
Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
//会立即返回,那么foo会被Dispose掉,所以会抛出异常
return foo.DoAnotherThingAsync();
}
}
//可以正常工作
async Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return await foo.DoAnotherThingAsync();
}
}
避免使用 .Wait() 或者 .Result,使用.GetAwaiter().GetResult() 代替
使用.Wait() 或者 .Result有在 GUI 应用程序中发生死锁
默认中,当一个未完成的 Task 被await的时候,当前的上下文将会在该Task完成的时候重新获得并继续执行剩余的代码。这个context就是当前的SynchronizationContext ,除非它是空的。GUI应用程序的SynchronizationContext有排他性,只允许一个线程运行。
public class DeadlockClass
{
private static async Task DelayAsync()
{
await Task.Delay(1000);
}
// 当这个方法访问界面元素时,哈哈,死锁就出来了
public static void Test()
{
// 开始delay
var delayTask = DelayAsync();
// 等待Delay的结束
delayTask.Wait();
}
}
private void TextButton_OnClick(object sender, RoutedEventArgs e)
{
DeadlockClass.Test();
TextButton.Content = "Helius";
}
在 UI 线程上调用 DeadlockClass.Test(),当 await 完成的时候,它试图在它原来的代码上下文(UI线程)执行它剩余的部分,但是该代码上下文已经有一个线程在了,就是那个一直在同步等待 async 完成的那个线程,它们两个相互等待,然后就死锁了。
另外一点需要注意的是,控制台并不会导致死锁,控制台的SynchronizationContext 类似于一个线程池的机制而不是排他的。因此当await结束的时候,它可以重新获得原来的上下文然后执行完剩余代码。这个不同是很多人产生困惑的根源,当他们在控制台测试的时候程序还OK,用GUI程序跑就发生了死锁。
使用.Wait() 或者 .Result将会将异常封装在AggregateException中,这会增加错误处理的复杂性。
class Program
{
//MainAsync 中的 try/catch 会捕获特定异常类型,但是如果将 try/catch 置于 Main 中,则它会始终捕获 AggregateException。
static void Main(string[] args)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
MainAsync().Wait();
Console.ReadKey();
}
static async Task MainAsync()
{
try
{
await Task.Delay(1000);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
}
catch (Exception ex)
{
}
}
}
高级的.GetAwaiter().GetResult() 将返回一个常规的异常
public void GetAwaiterGetResultExample()
{
//This is ok, but if an error is thrown, it will be encapsulated in an AggregateException
string data = GetData().Result;
//This is better, if an error is thrown, it will be contained in a regular Exception
data = GetData().GetAwaiter().GetResult();
}
异步库方法应该考虑使用Task.ConfigureAwait(false)来提高性能、避免死锁
随着异步 GUI 应用程序使用,可能会发现async方法的许多小部件都在使用GUI线程作为其上下文。这可能会形成迟滞。 ConfigureAwait可以提高性能 。
public class ConfigureAwaitFalse
{
async Task MyMethodAsync()
{
//这里还是调用线程的上下文(context)
await Task.Delay(1000);
//这里还是调用线程的上下文
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
//这里的上下文就不会是调用线程的上下文了,而是随机的上下文
//do Something
}
}
上面的代码与注释已经非常明显了。所以有一点是需要注意的,就是如果方法中在await后还需要用到上下文的,就不能设置ConfigureAwait(false),比如GUI程序中,如果方法中await后还有一些界面元素的操作,就会抛出线程异常了。
private async void TextButton_OnClick(object sender, RoutedEventArgs e)
{
TextButton.IsEnabled = false;
try
{
await Task.Delay(2000).ConfigureAwait(false);
}
finally
{
TextButton.IsEnabled = true;//这里会抛出异常
}
}
可以修改为:
private async void TextButton_OnClick(object sender, RoutedEventArgs e)
{
TextButton.IsEnabled = false;
try
{
//await Task.Delay(2000).ConfigureAwait(false);
await HandleClickAsync();
}
finally
{
TextButton.IsEnabled = true;
}
}
private async Task HandleClickAsync()
{
await Task.Delay(2000).ConfigureAwait(false);
}
Task 完成后,synchronization context(同步上下文)将调用post() 方法恢复到原来的位置。 但是,在编写库代码时,很少需要返回到以前的上下文。当使用 task.configureawait(false) 时,代码将不再尝试恢复到以前的位置。这稍微提高了性能,并有助于避免死锁。