上一篇,我给大家介绍了“.NET1.0 IAsyncResult
异步编程模型(APM)”,通过Begin***
开启操作并返回IAsyncResult
对象,使用 End***
方法来结束操作,通过回调方法来做异步操作后其它事项。然而最大的问题是没有提供进度通知等功能及多线程间控件的访问。为克服这个问题(并解决其他一些问题),.NET2.0 中引入了:基于事件的异步编程模式(EAP,Event-based Asynchronous Pattern
)。通过事件、AsyncOperationManager
类和AsyncOperation
类两个帮助器类实现如下功能:
1) 异步执行耗时的任务。
2) 获得进度报告和增量结果。
3) 支持耗时任务的取消。
4) 获得任务的结果值或异常信息。
5) 更复杂:支持同时执行多个异步操作、进度报告、增量结果、取消操作、返回结果值或异常信息。
对于相对简单的多线程应用程序,BackgroundWorker
组件提供了一个简单的解决方案。对于更复杂的异步应用程序,可以考虑实现一个符合基于事件的异步模式的类。
源码下载:异步编程:基于事件的异步模型(EAP).rar
EAP是为Windows窗体开发人员创建的,其主要优点在于:
EAP与Microsoft Visual Studio UI设计器进行了很好的集成。也就是说,可将大多数实现了EAP的类拖放到一个Visual Studio设计器平面上,然后双击事件名,让Visual Studio自动生成事件回调方法,并将方法同事件关联起来。
EAP类在内部通过SynchronizationContext
类,将应用程序模型映射到合适线程处理模型,以方便跨线程操作控件。
为了实现基于事件的异步模式,我们必须先理解两个重要的帮助器类:
AsyncOperationManager
类和AsyncOperation
类是System.ComponentModel
命名空间为我们提供了两个重要帮助器类。在基于事件的异步模式封装标准化的异步功能中,它确保你的异步操作支持在各种应用程序模型(包括 ASP.NET、控制台应用程序和 Windows 窗体应用程序)的适当“线程或上下文”调用客户端事件处理程序。
AsyncOperationManager
类和AsyncOperation
类的API如下:
// 为支持异步方法调用的类提供并发管理。此类不能被继承。
public static class AsyncOperationManager
{
// 获取或设置用于异步操作的同步上下文。
public static SynchronizationContext SynchronizationContext { get; set; }
// 返回可用于对特定异步操作的持续时间进行跟踪的AsyncOperation对象。
// 参数:userSuppliedState:
// 一个对象,用于使一个客户端状态(如任务 ID)与一个特定异步操作相关联。
public static AsyncOperation CreateOperation(object userSuppliedState)
{
return AsyncOperation.CreateOperation(userSuppliedState,SynchronizationContext);
}
}
// 跟踪异步操作的生存期。
public sealed class AsyncOperation
{
// 构造函数
private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext);
internal static AsyncOperation CreateOperation(object userSuppliedState
, SynchronizationContext syncContext);
// 获取传递给构造函数的SynchronizationContext对象。
public SynchronizationContext SynchronizationContext { get; }
// 获取或设置用于唯一标识异步操作的对象。
public object UserSuppliedState { get; }
// 在各种应用程序模型适合的线程或上下文中调用委托。
public void Post(SendOrPostCallback d, object arg);
// 结束异步操作的生存期。
public void OperationCompleted();
// 效果同调用 Post() + OperationCompleted() 方法组合
public void PostOperationCompleted(SendOrPostCallback d, object arg);
}
先分析下这两个帮助器类:
AsyncOperationManager
是静态类。静态类是密封的,因此不可被继承。倘若从静态类继承会报错“静态类必须从 Object
派生”。(小常识,以前以为密封类就是 sealed
关键字)
AsyncOperationManager
为支持异步方法调用的类提供并发管理,该类可正常运行于 .NET Framework 支持的所有应用程序模式下。
AsyncOperation
实例提供对特定异步任务的生存期进行跟踪。可用来处理任务完成通知,还可用于在不终止异步操作的情况下发布进度报告和增量结果(这种不终止异步操作的处理是通过AsyncOperation
的 Post()
方法实现)。
AsyncOperation
类有一个私有的构造函数和一个内部CreateOperation()
静态方法。由AsyncOperationManager
类调用AsyncOperation.CreateOperation()
静态方法来创建AsyncOperation
实例。
AsyncOperation
类是通过SynchronizationContext
类来实现在各种应用程序的适当“线程或上下文”调用客户端事件处理程序。
// 提供在各种同步模型中传播同步上下文的基本功能。
public class SynchronizationContext
{
// 获取当前线程的同步上下文。
public static SynchronizationContext Current { get; }
// 当在派生类中重写时,响应操作已开始的通知。
public virtual void OperationStarted();
// 当在派生类中重写时,将异步消息调度到一个同步上下文。
public virtual void Post(SendOrPostCallback d, object state);
// 当在派生类中重写时,响应操作已完成的通知。
public virtual void OperationCompleted();
……
}
a) 在
AsyncOperation
构造函数中调用SynchronizationContext
的OperationStarted()
;b) 在
AsyncOperation
的Post()
方法中调用SynchronizationContext
的Post()
;
c) 在AsyncOperation
的OperationCompleted()
方法中调用SynchronizationContext
的OperationCompleted()
;
SendOrPostCallback
委托签名:// 表示在消息即将被调度到同步上下文时要调用的方法。
public delegate void SendOrPostCallback(object state);
1.基于事件的异步模式可以采用多种形式,具体取决于某个特定类支持操作的复杂程度:
1) 最简单的类可能只有一个
***Async
方法和一个对应的***Completed
事件,以及这些方法的同步版本。2) 复杂的类可能有若干个
***Async
方法,每种方法都有一个对应的***Completed
事件,以及这些方法的同步版本。3) 更复杂的类还可能为每个异步方法支持取消(
CancelAsync()
方法)、进度报告和增量结果(ReportProgress()
方法+ProgressChanged
事件)。4) 如果您的类支持多个异步方法,每个异步方法返回不同类型的数据,您应该:
a) 将您的增量结果报告与您的进度报告分开。
b) 使用适当的EventArgs
为每个异步方法定义一个单独的***ProgressChanged
事件以处理该方法的增量结果数据。5) 如果类不支持多个并发调用,请考虑公开
IsBusy
属性。6) 如要异步操作的同步版本中有
Out
和Ref
参数,它们应做为对应***CompletedEventArgs
的一部分,eg:
public int MethodName(string arg1, ref string arg2, out string arg3);
public void MethodNameAsync(string arg1, string arg2);
public class MethodNameCompletedEventArgs : AsyncCompletedEventArgs
{
public int Result { get; };
public string Arg2 { get; };
public string Arg3 { get; };
}
2.如果你的组件要支持多个异步耗时的任务并行执行。那么:
1) 为***Async方法多添加一个userState对象参数(此参数应当始终是***Async方法签名中的最后一个参数),用于跟踪各个操作的生存期。
2) 注意要在你构建的异步类中维护一个userState
对象的集合。使用 lock
区域保护此集合,因为各种调用都会在此集合中添加和移除userState
对象。
3) 在***Async
方法开始时调用AsyncOperationManager.CreateOperation
并传入userState
对象,为每个异步任务创建AsyncOperation
对象,userState
存储在AsyncOperation
的UserSuppliedState
属性中。在构建的异步类中使用该属性标识取消的操作,并传递给CompletedEventArgs
和ProgressChangedEventArgs
参数的UserState
属性来标识当前引发进度或完成事件的特定异步任务。
4) 当对应于此userState
对象的任务引发完成事件时,你构建的异步类应将AsyncCompletedEventArgs.UserState
对象从集合中删除。
3.异常处理
EAP的错误处理和系统的其余部分不一致。首先,异常不会抛出。在你的事件处理方法中,必须查询AsyncCompletedEventArgs
的Exception
属性,看它是不是null
。如果不是null
,就必须使用if语句判断Exception
派生对象的类型,而不是使用catch
块。
另外,如果你的代码忽略错误,那么不会发生未处理的异常,错误会变得未被检测到,应用程序将继续运行,其结果不可预知。
4.注意:
1) 确保
***EventArgs
类特定于***
方法。即当使用***EventArgs
类时,切勿要求开发人员强制转换类型值。2) 确保始终引发方法名称
Completed
事件。成功完成、异常或者取消时应引发此事件。任何情况下,应用程序都不应遇到这样的情况:应用程序保持空闲状态,而操作却一直不能完成。3) 确保可以捕获异步操作中发生的任何异常并将捕获的异常指派给
Error
属性。4) 确保
***CompletedEventArgs
类将其成员公开为只读属性而不是字段,因为字段会阻止数据绑定。eg:public MyReturnType Result { get; }
5) 在构建
***CompletedEventArgs
类属性时,通过this.RaiseExceptionIfNecessary()
方法确保属性值被正确使用。Eg:
private bool isPrimeValue;
public bool IsPrime
{
get
{
RaiseExceptionIfNecessary();
return isPrimeValue;
}
}
所以,在***Completed
事件处理程序中,应当总是先检查 ***CompletedEventArgs.Error
和 ***CompletedEventArgs.Cancelled
属性,然后再访问RunWorkerCompletedEventArgs.Result
属性。
BackgroundWorker
组件System.ComponentModel
命名空间的BackgroundWorker
组件为我们提供了一个简单的多线程应用解决方案,它允许你在单独的线程上运行耗时操作而不会导致用户界面的阻塞。但是,要注意它同一时刻只能运行一个异步耗时操作(使用IsBusy
属性判定),并且不能跨AppDomain
边界进行封送处理(不能在多个AppDomain
中执行多线程操作)。
1.BackgroundWorker
组件
public class BackgroundWorker : Component
{
public BackgroundWorker();
// 获取一个值,指示应用程序是否已请求取消后台操作。
public bool CancellationPending { get; }
// 获取一个值,指示BackgroundWorker是否正在运行异步操作。
public bool IsBusy { get; }
// 获取或设置一个值,该值指示BackgroundWorker能否报告进度更新。
public bool WorkerReportsProgress { get; set; }
// 获取或设置一个值,该值指示BackgroundWorker是否支持异步取消。
public bool WorkerSupportsCancellation { get; set; }
// 调用RunWorkerAsync() 时发生。
public event DoWorkEventHandlerDoWork;
// 调用ReportProgress(System.Int32) 时发生。
public event ProgressChangedEventHandlerProgressChanged;
// 当后台操作已完成、被取消或引发异常时发生。
public event RunWorkerCompletedEventHandlerRunWorkerCompleted;
// 请求取消挂起的后台操作。
public void CancelAsync();
// 引发ProgressChanged事件。percentProgress:范围从 0% 到 100%
public void ReportProgress(int percentProgress);
// userState:传递到RunWorkerAsync(System.Object) 的状态对象。
public void ReportProgress(int percentProgress, object userState);
// 开始执行后台操作。
public void RunWorkerAsync();
// 开始执行后台操作。argument:传递给DoWork事件的DoWorkEventArgs参数。
public void RunWorkerAsync(object argument);
}
2.相应的EventArgs
类
///1) System.EventArgs基类
// System.EventArgs是包含事件数据的类的基类。
public class EventArgs
{
// 表示没有事件数据的事件。
public static readonly EventArgs Empty;
public EventArgs();
}
///2) DoWorkEventArgs类
// 为可取消的事件提供数据。
public class CancelEventArgs : EventArgs
{
public CancelEventArgs();
public CancelEventArgs(bool cancel);
// 获取或设置指示是否应取消事件的值。
public bool Cancel { get; set; }
}
// 为DoWork事件处理程序提供数据。
public class DoWorkEventArgs : CancelEventArgs
{
public DoWorkEventArgs(object argument);
// 获取表示异步操作参数的值。
public object Argument { get; }
// 获取或设置表示异步操作结果的值。
public object Result { get; set; }
}
///3) ProgressChangedEventArgs类
// 为ProgressChanged事件提供数据。
public class ProgressChangedEventArgs : EventArgs
{
public ProgressChangedEventArgs(int progressPercentage, object userState);
// 获取异步任务的进度百分比。
public int ProgressPercentage { get; }
// 获取唯一的用户状态。
public object UserState { get; }
}
///4) RunWorkerCompletedEventArgs类
// 为MethodNameCompleted事件提供数据。
public class AsyncCompletedEventArgs : EventArgs
{
public AsyncCompletedEventArgs();
public AsyncCompletedEventArgs(Exception error, bool cancelled, object userState);
// 获取一个值,该值指示异步操作是否已被取消。
public bool Cancelled { get; }
// 获取一个值,该值指示异步操作期间发生的错误。
public Exception Error { get; }
// 获取异步任务的唯一标识符。
public object UserState { get; }
// 访问 AsyncCompletedEventArgs 及其派生类的属性前调用此方法
protected void RaiseExceptionIfNecessary()
{
if (this.Error != null)
{
throw new TargetInvocationException(……);
}
if (this.Cancelled)
{
throw new InvalidOperationException(……);
}
}
}
public class RunWorkerCompletedEventArgs : AsyncCompletedEventArgs
{
public RunWorkerCompletedEventArgs(object result, Exception error, bool cancelled);
// 获取表示异步操作结果的值。
public object Result { get; }
// 获取表示用户状态的值。
public object UserState { get; }
}
3.BackgroundWorker
示例
示例代码中包含了BackgroundWorker
源代码及对应的使用示例,这里不粘贴代码了,会导致篇幅更大。来个示例截图吧:
示例分析:
1) 首先我们为BackgroundWorker组件注册DoWork(异步操作)、ProgressChanged(进度报告) 和RunWorkCompleted(完成通知)事件;
2) 设置WorkerSupportsCancellation和WorkerReportsProgress属性为true,以声明组件支持取消操作和进度报告;
3) 使用RunWorkerAsync() 开启异步操作,通过IsBusy属性判断是否已经有异步任务在执行;
4) 使用CancelAsync() 方法取消异步操作,但要注意:
a) 它仅仅是将BackgroudWorker.CancellationPending属性设置为true。需要在具体DoWork事件中不断检查BackgroudWorker.CancellationPending来设置DoWorkEventArgs的Cancel属性。
b) DoWork事件处理程序中的代码有可能在发出取消请求时完成其工作,轮询循环可能会错过设置为 true 的CancellationPending属性。在这种情况下,即使发出了取消请求,RunWorkerCompleted事件处理程序中RunWorkerCompletedEventArgs的 Cancelled 标志也不会设置为 true。这种情况被称作争用状态。(可以通过直接监控组件的CancellationPending属性,来做判断)
5) 确保在DoWork事件处理程序中不操作任何用户界面对象。而应该通过ProgressChanged和RunWorkerCompleted事件与用户界面进行通信。
因为RunWorkerAsync()
是通过委托的BeginInvoke() 引发的DoWork事件,即DoWork事件的执行线程已不是创建控件的线程(我在《异步编程:异步编程模型 (APM)》中介绍了几种夸线程访问控件的方式)。而ProgressChanged和RunWorkerCompleted事件是通过帮助器类AsyncOperation的 Post() 方法使其调用发生在合适的“线程或上下文”中。
刚才我们介绍了BackgroundWorker组件,但是这个组件在一个时刻只能开启一个异步操作,那如果我们要想同时支持多个异步操作、进度报告、增量结果、取消和返回结果值或异常信息该怎么办呢?对的,我们可以为自己定义一个基于事件的异步组件。
我直接引用MSDN上的一则计算质数的异步组件示例,请从我提供的示例代码中获取。
质数算法:埃拉托色尼筛法
eg:判断n是否为质数
1、1和0既非素数也非合数;
2、将2和3加入质数集合primes;从n=5开始,通过 n+=2 来跳过所有偶数;
3、循环集合primes中的质数并将其做为n的因子,能整除的为合数;
4、若不能整除,则继续循步骤3直到“因子的平方>n”,即可判断n为质数,并将其加入到集合primes。
来个示例截图吧:
示例分析:(组件名:PrimeNumberCalculator
)
首先我们为PrimeNumberCalculator
组件注册ProgressChanged
(进度报告) 和CalculatePrimeCompleted
(完成通知)事件;
使用CalculatePrimeAsync(intnumberToTest, object taskId)
开启异步任务,注意我们需要传递一个唯一标识Guid taskId = Guid.NewGuid();
用于标识取消的操作,并传递给CompletedEventArgs
和ProgressChangedEventArgs
参数的UserState
属性来标识当前引发进度或完成事件的特定异步任务;
取消操作CancelAsync(object taskId)
,只是将taskId
对应的AsyncOperation
实例移除内部任务集合,耗时操作通过判断taskId是否存在于集合来判断其是否被取消;
此文到此结束,通过此博文我们认识到:
1) 基于事件的异步编程是通过
AsyncOperationManager
类和AsyncOperation
类两个帮助器类确保你的异步操作支持在各种应用程序模型(包括 ASP.NET、控制台应用程序和 Windows 窗体应用程序)的适当“线程或上下文”调用访问控件;2)
BackgroundWorker
组件构建、使用和缺点。3) 展现如何构建一个基于事件的异步组件,并且支持多个异步操作的并行运行