系列文章目录
1. C#与C++的发展历程第一 - 由C#3.0起
2. C#与C++的发展历程第二 - C#4.0再接再厉
3. C#与C++的发展历程第三 - C#5.0异步编程的巅峰
C#5.0作为第五个C#的重要版本,将异步编程的易用度推向一个新的高峰。通过新增的async和await关键字,几乎可以使用编写同步代码的方式来编写异步代码。
本文将重点介绍下新版C#的异步特性以及部分其他方面的改进。同时也将介绍WinRT程序一些异步编程的内容。
C# async/await异步编程
写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这对方法:
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对象的创建就可以看出:
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方法:
task1.Start();这样task对象将在默认的TaskScheduler调度下去执行,TaskScheduler使用线程池中的线程,至于是新建还是使用已有线程这个对用户是完全透明的。还也可以通过重载函数的参数传入自定义的TaskScheduler。
关于TaskScheduler的调度,推荐园子里这篇文章,前半部分介绍了一些线程执行机制,很值得一度。
当我们用new创建一个Task对象时,创建的对象是Created状态,调用Start方法后将变为WaitingToRun状态。至于什么时候开始执行(进入Running状态,由TaskScheduler控制,)。Task的创建执行还有一种“快捷方式”,即Run方法:
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状态结束,会抛出导致任务失败的异常。