C#5.0作为第五个C#的重要版本,将异步编程的易用度推向一个新的高峰。通过新增的async和await关键字,几乎可以使用编写同步代码的方式来编写异步代码。
本文将重点介绍下新版C#的异步特性以及部分其他方面的改进。同时也将介绍WinRT程序一些异步编程的内容。
写async异步编程这部分内容之前看了好多文章,反复整理自己的思路,尽力保证文章的正确性。尽管如此仍然可能存在错误,请广大园友及时指出,感谢感谢。
异步编程不是一个新鲜的话题,最早期的C#版本也内建对异步编程的支持,当然在颜值上无法与目前基于TAP,使用async/await的异步编程相比。异步编程要解决的问题就是许多耗时的IO可能会阻塞线程导致CPU空转降低效率,或者一个长时间的后台任务会阻塞用户界面。通过将耗时任务异步执行来使系统有更高的吞吐量,或保持界面的响应能力。如界面在加载一幅来自网络的图像时,还运行用户进行其他操作。
按前文惯例先上一张图通览一下TAP模式下异步编程的方方面面,然后由异步编程的发展来讨论一下TAP异步模式。
图1
APM
C# .NET最早出现的异步编程模式被称为APM(Asynchronous Programming Model)。这种模式主要由一对Begin/End开头的组成。BeginXXX方法用于启动一个耗时操作(需要异步执行的代码段),相应的调用EndXXX来结束BeginXXX方法开启的异步操作。BeginXXX方法和EndXXX方法之间的信息通过一个IAsyncResult对象来传递。这个对象是BeginXXX方法的返回值。如果直接调用EndXXX方法,则将以阻塞的方式去等待异步操作完成。另一种更好的方法是在BeginXXX倒数第二个参数指定的回调函数中调用EndXXX方法,这个回调函数将在异步操作完成时被触发,回调函数的第二个参数即EndXXX方法所需要的IAsyncResult对象。
.NET中一个典型的例子如System.Net命名空间中的HttpWebRequest类里的BeginGetResponse和EndGetResponse这对方法:
1
2
|
IAsyncResult BeginGetResponse(AsyncCallback callback, object state) WebResponse EndGetResponse(IAsyncResult asyncResult) |
由方法声明即可看出,它们符合前述的模式。
APM使用简单明了,虽然代码量稍多,但也在合理范围之内。APM两个最大的缺点是不支持进度报告以及不能方便的“取消”。
EAP
在C# .NET第二个版本中,增加了一种新的异步编程模型EAP(Event-based Asynchronous Pattern),EAP模式的异步代码中,典型特征是一个Async结尾的方法和Completed结尾的事件。XXXCompleted事件将在异步处理完成时被触发,在事件的处理函数中可以操作异步方法的结果。往往在EAP代码中还会存在名为CancelAsync的方法用来取消异步操作,以及一个ProgressChenged结尾的事件用来汇报操作进度。通过这种方式支持取消和进度汇报也是EAP比APM更有优势的地方。通过后文TAP的介绍,你会发现EAP中取消机制没有可延续性,并且不是很通用。
.NET2.0中新增的BackgroundWorker可以看作EAP模式的一个例子。另一个使用EAP的例子是被HttpClient所取代的WebClient类(新代码应该使用HttpClient而不是WebClient)。WebClient类中通过DownloadStringAsync方法开启一个异步任务,并有DownloadStringCompleted事件供设置回调函数,还能通过CancelAsync方法取消异步任务。
TAP & async/await
从.NET4.0开始新增了一个名为TPL的库主要负责异步和并行操作的处理,目标就是使异步和并发操作有个统一的操作界面。TPL库的核心是Task类,有了Task几乎不用像之前版本的异步和并发那样去和Thread等底层类打交道,作为使用者的我们只需要处理好Task,Task背后有一个名为的TaskScheduler的类来处理Task在Thread上的执行。可以这样说TaskScheduler和Task就是.NET4.0中异步和并发操作的基础,也是我们写代码时不二的选择。
对于Task可以将其理解为一个包装委托对象(通常就是Action或Func对象)并执行的容器,从Task对象的创建就可以看出:
1
2
3
4
5
|
Action action = () => Console.WriteLine( "Hello World" ); Task task1 = new Task(action); Func< object , string > func = name => "Hello World" + name; Task< string > task2 = new Task< string >(func, "hystar" , CancellationToken.None,TaskCreationOptions.None ); //接收object参数真蛋疼,很不容易区分重载,把参数都写上吧。 |
执行这个Task对象需要手动调用Start方法:
1
|
task1.Start(); |
这样task对象将在默认的TaskScheduler调度下去执行,TaskScheduler使用线程池中的线程,至于是新建还是使用已有线程这个对用户是完全透明的。还也可以通过重载函数的参数传入自定义的TaskScheduler。
关于TaskScheduler的调度,推荐园子里这篇文章,前半部分介绍了一些线程执行机制,很值得一度。
当我们用new创建一个Task对象时,创建的对象是Created状态,调用Start方法后将变为WaitingToRun状态。至于什么时候开始执行(进入Running状态,由TaskScheduler控制,)。Task的创建执行还有一种“快捷方式”,即Run方法:
1
2
|
Task.Run(() => Console.WriteLine( "Hello World" )); var txt = await Task< string >.Run(() => "Hello World" ); |
这种方式创建的Task会直接进入WaitingToRun状态。
Task的其他状态还有RanToCompletion,Canceled以及Faulted。在到大RanToCompletion状态时就可以获得Task<T>类型任务的结果。如果Task在状态为Canceled的情况下结束,会抛出 OperationCanceledException。如果以Faulted状态结束,会抛出导致任务失败的异常。
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同时也能理解这种模式下异常和取消是怎样完成的。
先来展示下用于分析反编译代码的例子,一个控制台项目的代码,这是能想到的展示异步方法最简单的例子了,而且和实际项目中常用的代码结构也差不太多:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
//实体类 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
热门源码