异步编程-为了更好的体验
动机(motivation)
如果一个操作可能需要消耗很长的时间, 使用同步编程模式的话, 那么UI线程会在操作执行的时间内被阻塞。 我们常见的就是应用程序没有响应的假死状态。 这种行为给用户的体验非常差, 是我们所要避免的。
假设AB公司要开发一个自动化订单生成程序。 AB公司的工程师一般需要首先生成设计工程, 在根据工程生成设计图纸。 然后在图纸中抽取出所有的无聊清单, 并根据无聊清单来生成订单文件。 后台开发者已经完成了代码如下。
class DoWork { /// <summary> /// indicate the Do work Complete status /// </summary> public int iStatus { get; set; } public void Run() { this.iStatus = 0; this.LoadConfiguration(); this.iStatus = 20; this.ValidateConfiguration(); this.iStatus = 40; this.GenerateOrder(); this.iStatus = 60; this.BuildBOM(); this.iStatus = 80; this.GenerateOrder(); this.iStatus = 100; } /// <summary> /// Load the configuration data, it is the engineer design /// </summary> private void LoadConfiguration() { Console.WriteLine("Loading Configuration...."); System.Threading.Thread.Sleep(2000); } private void ValidateConfiguration() { Console.WriteLine("Validating Configuration"); System.Threading.Thread.Sleep(2000); } /// <summary> /// Create Drawing base on engineer design /// </summary> private void CreateDrawing() { Console.WriteLine("Creating Drawing"); System.Threading.Thread.Sleep(2000); } /// <summary> /// Extract the matrial used in the design /// </summary> private void BuildBOM() { Console.WriteLine("Building BOM"); System.Threading.Thread.Sleep(2000); } /// <summary> /// Generate the Order, send to ERP or other format that organization preferred /// </summary> private void GenerateOrder() { Console.WriteLine("Generating Order"); System.Threading.Thread.Sleep(2000); } }
在上面的例子里面, 我们模拟了一个五个逻辑步骤的操作, 每个步骤耗时2秒。 同步模式下, 有10秒钟的时间主线程被阻塞。这是不可接受的行为。
实现:
将方法执行放在一个新的线程上执行。 工作线程因此不会阻塞UI线程。
比较简陋的实现时
多线程:
显示声明一个线程, 用线程来执行所需要执行的方法
System.Threading.Thread thread = new System.Threading.Thread(this.WorkFun); //workfun will point to the function that you want to run background thread.Start();
显式声明虽然处理简单,如果我们想取消这个线程, 我们只要执行 abort方法就可以退出线程。
但是问题是因为并行处理的关系, 我们不知道程序什么时候结束。 而且线程是一个“昂贵的”资源, 频繁的创建代价很高。
利用线程池技术, 我们可以把上面的程序改写如下。
线程池:
Console.WriteLine("Add Work funtion to Threading Pool"); ThreadPool.QueueUserWorkItem(this.WorkFun); // workfun is the function to execute background Console.WriteLine("Exit the Main function");
线程池技术很明显的规避了线程的创建和销毁的问题, 在频繁使用的时候效率更高。但是缺点也是明显的
但是我们依旧不能知道, 我们放在线程中的方法什么时候能结束。
而且因为线程是放置在线程池中, 我们并不知道是哪个具体的线程在工作, 所以我们也无法去取消我们不慎发起的操作。
试想, 一个耗时很长的操作, 却不能被取消, 这该是怎样的杯具。
[不能取消的异步操作不是好操作]
APM(asynchronous programming model)是时候隆重出场了。虽然已经不再是微软首推的异步编程模式, 但是笔者很喜欢因为它很简单而且容易使用。
APM的核心是一个methodcaller, 其实就是一个多播委托(multicastdelegate)
APM总是从一个begininvoke开始,从endinvoke结束。 当endinvoke所在的方法结束执行以后, 异步线程会被释放回线程池。
public delegate void MethodCaller(); public void MainFun() { DoWork objWork = new DoWork(); MethodCaller caller = new MethodCaller(objWork.Run); Console.WriteLine("Begin the Delegate function calling"); IAsyncResult rslt = caller.BeginInvoke( new AsyncCallback( CallBackMethod),null); Console.WriteLine("Complete Main Fun"); // caller.EndInvoke(rslt); } public void CallBackMethod(IAsyncResult rslt) { Console.WriteLine("Here I can know the method execution has completed"); AsyncResult result = (AsyncResult)rslt; MethodCaller caller = (MethodCaller)result.AsyncDelegate; Console.WriteLine(rslt.AsyncWaitHandle.SafeWaitHandle.ToString()); Console.WriteLine( "Try to End Invoke function" ); caller.EndInvoke(rslt); Console.WriteLine( "Complete end invokde function" ); }
APM是一个非常简单, 容易使用的方法, 但是问题是, Endinvoke真的需要嘛?
我们知道, 。net的垃圾回收是GC来完成, 所以即使我们声明释放了资源, 线程一定能被立即释放吗?
我的意见是,功能上说, 不需要。 性能上说, Endinvoke需要。 理由有2
1. 我们可以使用endinvoke知道异步调用什么时候结束。很多时候我们需要在调用结束以后开始下一步的工作。
2. Endinvoke可以显式告诉GC我们已经不需要线程相关的资源, 他能够让GC尽早回收线程相关的资源。 这一点在服务器程序上对性能的影响尤其重要。
上面的例子里面, 开发者需要对APM有所理解才能开发代码。 有没有一种方法, 让我们能够不需要知道异步编程也能写出异步调用的代码? 答案是。
EAP模式:
EAP的动机:在Basic和C的年代, 我们的代码都是从Main函数开始,一切都可以查询。 但是当VC来临的时候, 一切都变了, 你再也找不到Main函数了, 因为从VC开始, winform编程面向对象, UI界面面向事件。
在VC时代, 一切都变了。 程序员所要考虑的是控件的事件中要实现什么功能, 如果说 APM是C思维, 那么EAP就是VC思维。EAP向用户隐藏实现, 让用户专注于抽象, 允许用户扩展功能。
下面是一个简单的EAP内部实现。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.Remoting.Messaging; namespace ConsoleDemo.AsycPrg.EAP { /// <summary> /// Delegate for the Asyc running method. user can use this delegate to run the function they want to running /// </summary> /// <param name="e"></param> public delegate void AsycRunningHandler(EventArgs e); /// <summary> /// Delegate to indicate asyc function has completed, use can do next steps work here /// </summary> /// <param name="e"></param> public delegate void CompleteAsycRunningHandler(EventArgs e); /// <summary> /// a method caller, it will launch the aync calling /// </summary> public delegate void MethodCaller(); class EAPObject { public bool IsBusy { get; set; } public event AsycRunningHandler RunBackgroundWorkEvent; public event CompleteAsycRunningHandler CompleteBackgroundEvent; private MethodCaller Caller; public EAPObject() { this.IsBusy = false; } /// <summary> /// Consumer will call this function to start asyc work /// </summary> public void RunAsyncWork() { this.IsBusy = true; this.Caller = new MethodCaller(this.RunBackgrounMethod); this.Caller.BeginInvoke(new AsyncCallback(this.CompleteAsyncWork), null); } /// <summary> /// if the RunBackgroundWorkEvent is not empty, then execute the function belong to the delegate. /// </summary> public void RunBackgrounMethod() { if (this.RunBackgroundWorkEvent != null) { this.RunBackgroundWorkEvent(new EventArgs()); } } /// <summary> /// Execute the endinvode to finalize async calling /// </summary> /// <param name="rslt"></param> public void CompleteAsyncWork(IAsyncResult rslt) { if (this.CompleteBackgroundEvent != null) { this.CompleteBackgroundEvent(new EventArgs()); } AsyncResult result = (AsyncResult)rslt; MethodCaller caller = (MethodCaller)result.AsyncDelegate; caller.EndInvoke(rslt); this.IsBusy = false; } } }
上面的例子, EAPObject 类像用户暴露了两个event,通过 RunBackgroundWorkEvent用户可以所要异步执行的方法。 CompleteBackgroundEvent可以让用户实现在得知任务完成之后的操作。 调用端就简单了。
static void Main(string[] args) { EAPObject obj = new EAPObject(); obj.RunBackgroundWorkEvent += new AsycPrg.EAP.AsycRunningHandler(obj_RunBackgroundWorkEvent); obj.CompleteBackgroundEvent += new AsycPrg.EAP.CompleteAsycRunningHandler(obj_CompleteBackgroundEvent); Console.WriteLine("Complete setting"); obj.RunAsyncWork(); Console.WriteLine("Have exit the RunAsyncwork function"); Console.ReadLine(); } static void obj_RunBackgroundWorkEvent(EventArgs e) { Console.WriteLine("New do work Instance"); AsycPrg.DoWork objwork = new AsycPrg.DoWork(); Console.WriteLine("Run the Do Work function"); objwork.Run(); Console.WriteLine("Complete Run function"); } static void obj_CompleteBackgroundEvent(EventArgs e) { Console.WriteLine("Now I know the background work is completed"); //throw new NotImplementedException(); } }
客户端的调用, 用户只要指定事件的回调方法, 不用关心这两个方法什么时候被调用, 怎样被调用。
从原理上说, EAP是对APM的包装, 所以EAP从效率上说肯定不如APM。 相对于EAP来说, APM更加简单更加容易使用。 但是EAP一般都是以控件形式来出现, 所以实际上喜欢EAP的也不少。
很不幸, 不管是EAP还是APM, 都已经不再是微软首推的异步编程模式了。 在net4.0时代, 基于任务的异步编程模式成为了主流(TAP)。很抱歉向大家说了一堆过时的东西。
个人觉得, 学习一种技术, 我们需要掌握的是他的推出动机以及这种方法所能给我们所带来的好处。下一篇我们将来讨论一下目前的主流TAP(实际上也不是主流了, 5.0里面 async关键字提供了一个更加简洁的异步模式)