异步编程模型(APM)是.NET1.0的时候已经推出的古老异步编程模式,此模式基于IAsyncResult接口实现。
随着技术的发展,又在“.NET1.0异步编程模型(APM)”之后推出了“.NET2.0基于事件的编程模式”及“.NET4.X基于任务的编程模式”两种异步编程模式。尽管在新的设计上我们推荐都使用“.NET4.0基于任务的编程模式”,但我还是计划整理出旧版的异步编程模型,因为:
1、在一些特殊场合下我们可能觉得一种模式更适合;
2、可以更充分认识三种模式之间的优劣,便于选择;
3、很多遗留的代码包含了旧的设计模式;
4、等等…
示例下载:异步编程:IAsyncResult异步编程模型.rar
IAsyncResult设计模式----规范概述
使用IAsyncResult设计模式的异步操作是通过名为 Begin*** 和 End*** 的两个方法来实现的,这两个方法分别指代开始和结束异步操作。例如,FileStream类提供BeginRead和EndRead方法来从文件异步读取字节。这两个方法实现了 Read 方法的异步版本。
在调用 Begin*** 后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。(如果有返回值还应调用 End*** 来获取操作的结果)。
1)Begin***
a)Begin*** 方法带有该方法的同步版本签名中声明的任何参数。
b)Begin*** 方法签名中不包含任何输出参数。方法签名后两个参数的规范是:第一个参数定义一个AsyncCallback委托,此委托引用在异步操作完成时调用的方法。第二个参数是一个用户定义的对象。此对象可用来向异步操作完成时为AsyncCallback委托方法传递应用程序特定的状态信息(eg:可通过此对象在委托中访问End*** 方法)。另外,这两个参数都可以传递null。
c)返回IAsyncResult对象。 // 表示异步操作的状态。 [ComVisible(true)] public interface IAsyncResult { // 获取用户定义的对象,它限定或包含关于异步操作的信息。 object AsyncState { get; } // 获取用于等待异步操作完成的System.Threading.WaitHandle,待异步操作完成时获得信号。 WaitHandle AsyncWaitHandle { get; } // 获取一个值,该值指示异步操作是否同步完成。 bool CompletedSynchronously { get; } // 获取一个值,该值指示异步操作是否已完成。 bool IsCompleted { get; } } // 常用委托声明(我后面示例是使用了自定义的带ref参数的委托) public delegate void AsyncCallback(IAsyncResult ar)
2)End***
a)End*** 方法可结束异步操作,如果调用 End*** 时,IAsyncResult对象表示的异步操作还未完成,则 End*** 将在异步操作完成之前阻塞调用线程。
b)End*** 方法的返回值与其同步副本的返回值类型相同。End*** 方法带有该方法同步版本的签名中声明的所有out 和 ref 参数以及由BeginInvoke返回的IAsyncResult,规范上 IAsyncResult 参数放后。
i. 要想获得返回结果,必须调用的方法;
ii. 若带有out 和 ref 参数,实现上委托也要带有out 和 ref 参数,以便在回调中获得对应引用传参值做相应逻辑;
现在我们清楚了IAsyncResult设计模式的设计规范,接下来我们再通过IAsyncResult异步编程模式的三个经典场合来加深理解。
一、基于IAsyncResult构造一个异步API
现在来构建一个IAsyncResult的类,并且实现异步调用。 // 带ref参数的自定义委托 public delegate void RefAsyncCallback(ref string resultStr, IAsyncResult ar); public class CalculateAsyncResult : IAsyncResult { private int _calcNum1; private int _calcNum2; private RefAsyncCallback _userCallback; public CalculateAsyncResult(int num1, int num2, RefAsyncCallback userCallback, object asyncState) { this._calcNum1 = num1; this._calcNum2 = num2; this._userCallback = userCallback; this._asyncState = asyncState; // 异步执行操作 ThreadPool.QueueUserWorkItem((obj) => { AsyncCalculate(obj); }, this); } #region IAsyncResult接口 private object _asyncState; public object AsyncState { get { return _asyncState; } } private ManualResetEvent _asyncWaitHandle; public WaitHandle AsyncWaitHandle { get { if (this._asyncWaitHandle == null) { ManualResetEvent event2 = new ManualResetEvent(false); Interlocked.CompareExchange<ManualResetEvent>(ref this._asyncWaitHandle, event2, null); } return _asyncWaitHandle; } } private bool _completedSynchronously; public bool CompletedSynchronously { get { return _completedSynchronously; } } private bool _isCompleted; public bool IsCompleted { get { return _isCompleted; } } #endregion /// <summary> /// /// 存储后结果值 /// </summary> public int FinnalyResult { get; set; } /// <summary> /// End方法只应调用一次,超过一次报错 /// </summary> public int EndCallCount = 0; /// <summary> /// ref参数 /// </summary> public string ResultStr; /// <summary> /// 异步进行耗时计算 /// </summary> /// <param name="obj">CalculateAsyncResult实例本身</param> private static void AsyncCalculate(object obj) { CalculateAsyncResult asyncResult = obj as CalculateAsyncResult; Thread.SpinWait(1000); asyncResult.FinnalyResult = asyncResult._calcNum1 * asyncResult._calcNum2; asyncResult.ResultStr = asyncResult.FinnalyResult.ToString(); // 是否同步完成 asyncResult._completedSynchronously = false; asyncResult._isCompleted = true; ((ManualResetEvent)asyncResult.AsyncWaitHandle).Set(); if (asyncResult._userCallback != null) asyncResult._userCallback(ref asyncResult.ResultStr, asyncResult); } } public class CalculateLib { public IAsyncResult BeginCalculate(int num1, int num2, RefAsyncCallback userCallback, object asyncState) { CalculateAsyncResult result = new CalculateAsyncResult(num1, num2, userCallback, asyncState); return result; } public int EndCalculate(ref string resultStr, IAsyncResult ar) { CalculateAsyncResult result = ar as CalculateAsyncResult; if (Interlocked.CompareExchange(ref result.EndCallCount, 1, 0) == 1) { throw new Exception("End方法只能调用一次。"); } result.AsyncWaitHandle.WaitOne(); resultStr = result.ResultStr; return result.FinnalyResult; } public int Calculate(int num1, int num2, ref string resultStr) { resultStr = (num1 * num2).ToString(); return num1 * num2; } }
使用上面通过IAsyncResult设计模式实现的带ref引用参数的异步操作,我将展示三种阻塞式响应异步调用和一种无阻塞式委托响应异步调用。即:
1、执行异步调用后,若我们需要控制后续执行代码在异步操作执行完之后执行,可通过下面三种方式阻止其他工作:
a)IAsyncResult的AsyncWaitHandle属性,待异步操作完成时获得信号。
b)通过IAsyncResult的IsCompleted属性进行轮询。通过轮询还可实现进度条功能。
c)调用异步操作的 End*** 方法。
2、执行异步调用后,若我们不需要阻止后续代码的执行,那么我们可以把异步执行操作后的响应放到回调中进行。 public class Calculate_Test { public static void Test() { CalculateLib cal = new CalculateLib(); // 基于IAsyncResult构造一个异步API IAsyncResult calculateResult = cal.BeginCalculate(123, 456, AfterCallback, cal); // 执行异步调用后,若我们需要控制后续执行代码在异步操作执行完之后执行,可通过下面三种方式阻止其他工作: // 1、IAsyncResult 的 AsyncWaitHandle 属性,带异步操作完成时获得信号。 // 2、通过 IAsyncResult 的 IsCompleted 属性进行轮询。通过轮询还可实现进度条功能。 // 3、调用异步操作的 End*** 方法。 // *********************************************************** // 1、calculateResult.AsyncWaitHandle.WaitOne(); // 2、while (calculateResult.IsCompleted) { Thread.Sleep(1000); } // 3、 string resultStr = string.Empty; int result = cal.EndCalculate(ref resultStr, calculateResult); } /// <summary> /// 异步操作完成后做出响应 /// </summary> private static void AfterCallback(ref string resultStr, IAsyncResult ar) { // 执行异步调用后,若我们不需要阻止后续代码的执行,那么我们可以把异步执行操作后的响应放到回调中进行。 CalculateLib cal = ar.AsyncState as CalculateLib; //int result = cal.EndInvoke(ref resultStr, calculateResult1); //if (result > 0) { } } }
二、使用委托进行异步编程
对于委托,编译器会为我们生成同步调用方法“invoke”以及异步调用方法“BeginInvoke”和“EndInvoke”。对于异步调用方式,公共语言运行库 (CLR) 将对请求进行排队并立即返回到调用方,由线程池的线程调度目标方法并与提交请求的原始线程并行运行。
异步委托是快速为方法构建异步调用的方式,它基于IAsyncResult设计模式实现的异步调用,即,通过BeginInvoke返回IAsyncResult对象;通过EndInvoke获取结果值。
示例:
上节的CalculateLib类中的同步方法以及所要实用到的委托如下: // 带ref参数的自定义委托 public delegate int AsyncInvokeDel(int num1, int num2, ref string resultStr); public int Calculate(int num1, int num2, ref string resultStr) { resultStr = (num1 * num2).ToString(); return num1 * num2; }
然后,通过委托进行同步或异步调用: AsyncInvokeDel calculateAction = cal.Calculate; string resultStrAction = string.Empty; // int result1 = calculateAction.Invoke(123, 456, ref resultStrAction); IAsyncResult calculateResult1 = calculateAction.BeginInvoke(123, 456, ref resultStrAction, null, null); int result1 = calculateAction.EndInvoke(ref resultStrAction, calculateResult1);
三、多线程操作控件
访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。
不过,在有些情况下,您可能需要多线程调用控件的方法。.NET Framework 提供了从任何线程操作控件的方式:
1、非安全方式访问控件(此方式请永远不要再使用)
多线程访问窗口中的控件,可以在窗口的构造函数中将Form的CheckForIllegalCrossThreadCalls静态属性设置为false。 // 获取或设置一个值,该值指示是否捕获对错误线程的调用, // 这些调用在调试应用程序时访问控件的System.Windows.Forms.Control.Handle属性。 // 如果捕获了对错误线程的调用,则为 true;否则为 false。 public static bool CheckForIllegalCrossThreadCalls { get; set; }
2、安全方式访问控件
原理:从一个线程封送调用并跨线程边界将其发送到另一个线程,并将调用插入到创建控件线程的消息队列中,当控件创建线程处理这个消息时,会在自己的上下文中执行传入的方法。(此过程只有调用线程和创建控件线程,并没有创建新线程)
注意:从一个线程封送调用并跨线程边界将其发送到另一个线程会耗费大量的系统资源,所以应避免重复调用其他线程上的控件。
1)使用BackgroundWork后台辅助线程控件控件(AsyncOperationManager类和AsyncOperation类帮助器方式)。
2)结合TaskScheduler.FromCurrentSynchronizationContext() 和Task 实现。
3)使用Control类上提供的Invoke 和BeginInvoke方法。
因本文主要解说IAsyncResult异步编程模式,所以只详细分析Invoke 和BeginInvoke跨线程访问控件方式。
Control类实现了ISynchronizeInvoke接口,提供了Invoke和BeginInvoke方法来支持其它线程更新GUI界面控件的机制。 public interface ISynchronizeInvoke { // 获取一个值,该值指示调用线程是否与控件的创建线程相同。 bool InvokeRequired { get; } // 在控件创建的线程上异步执行指定委托。 AsyncResult BeginInvoke(Delegate method, params object[] args); object EndInvoke(IAsyncResult asyncResult); // 在控件创建的线程上同步执行指定委托。 object Invoke(Delegate method, params object[] args); }
1)Control类的 Invoke,BeginInvoke 内部实现如下:
a)Invoke (同步调用)先判断控件创建线程与当前线程是否相同,相同则直接调用委托方法;否则使用Win32API的PostMessage 异步执行。
b)BeginInvoke (异步调用)使用Win32API的PostMessage 异步执行 UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle) , threadCallbackMessage, IntPtr.Zero, IntPtr.Zero); [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern bool PostMessage(HandleRefhwnd, intmsg, IntPtrwparam, IntPtrlparam);
PostMessage 是windows api,用来把一个消息发送到一个窗口的消息队列。这个方法是异步的,也是该方法封送完毕后马上返回,不会等待委托方法的执行结束,调用者线程将不会被阻塞。(对应同步方法的windows api是:SendMessage())
2)InvokeRequired
获取一个值,该值指示调用线程是否与控件的创建线程相同。内部关键如下: Int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num); Int currentThreadId = SafeNativeMethods.GetCurrentThreadId(); return (windowThreadProcessId != currentThreadId);
即返回“通过GetWindowThreadProcessId功能函数得到创建指定窗口线程的标识和创建窗口的进程的标识符与当前线程Id进行比较”的结果。
3)示例(详见示例文件)
在使用的时候,我们使用 this.InvokeRequired 属性来判断是使用Invoke或BeginInvoke 还是直接调用方法。 private void InvokeControl(object mainThreadId) { if (this.InvokeRequired) { this.Invoke(new Action<String>(ChangeText), "InvokeRequired = true.改变控件Text值"); //this.textBox1.Invoke(new Action<int>(InvokeCount), (int)mainThreadId); } else { ChangeText("在创建控件的线程上,改变控件Text值"); } } private void ChangeText(String str) { this.textBox1.Text += str; }
注意,在InvokeControl方法中使用 this.Invoke(Delegate del) 和使用 this.textBox1.Invoke(Delegate del) 效果是一样的。因为在执行Invoke或BeginInvoke时,内部首先调用 FindMarshalingControl() 进行一个循环向上回溯,从当前控件开始回溯父控件,直到找到的父控件,用它作为封送对象。也是说 this.textBox1.Invoke(Delegate del) 会追溯到和 this.Invoke(Delegate del) 一样的起点。(子控件的创建线程一定是创建父控件的线程,所以这种追溯不会导致将调用封送到错误的目的线程)
本节到此结束,本节主要讲了异步编程模式之一“异步编程模型(APM)”,是基于IAsyncResult设计模式实现的异步编程方式,并且构建了一个继承自IAsyncResult接口的示例,及展示了这种模式在委托及跨线程访问控件上的经典应用。