Task同时服务于并发编程和异步编程(在Jeffrey Richter的CLR via C#中分别称这两种模式为计算限制的异步操作和IO限制的异步操作,仔细想想这称呼也很贴切),这里主要讨论下Task和异步编程的相关的机制。其中最关键的一点就是Task是一个awaitable对象,这是其可以用于异步编程的基础。除了Task,还有很多类型也是awaitable的,如ConfigureAwait方法返回的ConfiguredTaskAwaitable、WinRT平台中的IAsyncInfo(这个后文有详细说明)等。要成为一个awaitable类型需要符合哪些条件呢?其实就一点,其中有一个GetAwaiter()方法,该方法返回一个awaiter。那什么是awaiter对象呢?满足如下3点条件即可:
实现INotifyCompletion或ICriticalNotifyCompletion接口
有bool类型的IsCompleted属性
有一个GetResult()来返回结果,或是返回void
awaitable和awaiter的关系正如IEnumerable和IEnumerator的关系一样。推而广之,下面要介绍的async/await的幕后实现方式和处理yield语法糖的实现方式差不多。
Task类型的GetAwaiter()返回的awaiter是TaskAwaiter类型。这个TaskAwaiter很简单基本上就是刚刚满足上面介绍的awaiter的基本要求。类似于EAP,当异步操作执行完毕后,将通过OnCompleted参数设置的回调继续向下执行,并可以由GetResult获取执行结果。
简要了解过Task,再来看一下本节的重点 - async异步方法。async/await模式的异步也出来很久了,相关文章一大片,这里介绍下重点介绍下一些不容易理解和值得重点关注的点。我相信我曾经碰到的困惑也是很多人的遇到的困惑,写出来和大家共同探讨。
语法糖
对async/await有了解的朋友都知道这两个关键字最终会被编译为.NET中和异步相关的状态机的代码。这一部分来具体看一下这些代码,了解它们后我们可以更准确的去使用async/await同时也能理解这种模式下异常和取消是怎样完成的。
先来展示下用于分析反编译代码的例子,一个控制台项目的代码,这是能想到的展示异步方法最简单的例子了,而且和实际项目中常用的代码结构也差不太多:
//实体类 public class User { public int Id { get; set; } public string UserName { get; set; } = "hystar"; public string Email { get; set; } } class Program { static void Main(string[] args) { var service = new Service(new Repository()); var name = service.GetUserName(1).Result; Console.WriteLine(name); } } public class Service { private readonly Repository _repository; public Service(Repository repository) { _repository = repository; } public async Task<string> GetUserName(int id) { var name = await _repository.GetById(id); return name; } } public class Repository { private DbContext _dbContext; private DbSet<User> _set; public Repository() { _dbContext = new DbContext(""); _set = _dbContext.Set<User>(); } public async Task<string> GetById(int id) { //IO... var user = await _set.FindAsync(id); return user.UserName; } }注意:控制台版本的示例代码中在Main函数中使用了task.Result来获取异步结果,需要注意这是一种阻塞模式,在除控制台之外的UI环境不要使用类似Result属性这样会阻塞的方法,它们会导致UI线程死锁。而对于没有SynchronizationContext的控制台应用确是再合适不过了。对于没有返回值的Task,可以使用Wait()方法等待其完成。
这里使用ILSpy去查看反编译后的代码,而且注意要将ILSpy选项中的Decompile async methods (async/await)禁用(如下图),否则ILSpy会很智能将IL反编译为有async/await关键字的C#代码。另外我也尝试过Telerik JustDecompile等工具,但是能完整展示反编译出的状态机的只有ILSpy。
图2
另外注意,应该选择Release版本的代码去查看,这是在一个Stackoverflow回答中看到的,说是有啥不同,具体也没仔细看,这里知道选择Release版exe/dll反编译就好了。下面以Service类为例来看一下反编译后的代码:
图3
通过图上的注释可以看到代码主要由两大部分构成,Service类原有的代码和一个由编译器生成的状态机,下面分别具体了解下它们都做了什么。依然是以图片加注释为主,重要的部分会在图后给出文字说明。
图4
通过上图中的注释可以大致了解GetUserName方法编译后的样子。我们详细介绍下其中几个点,首先是AsyncTaskMethodBuilder<T>,我感觉很有必要列出其代码一看: