实现基于事件的异步模式的最佳做法
更新:2007 年 11 月
基于事件的异步模式提供了一种在类中使用熟悉的事件和委托语义公开异步行为的有效方法。若要实现基于事件的异步模式,您需要遵守一些特定的行为要求。下面几节描述了您在实现一个遵循基于事件的异步模式的类时应当考虑的要求和准则。
有关概述,请参见 实现基于事件的异步模式。
下面的列表说明了本主题讨论的最佳做法:
必需的行为保证
完成
完成的事件和 EventArgs
同时执行操作
访问结果
进度报告
IsBusy 实现
取消
错误和异常
线程处理和上下文
准则
必需的行为保证
若要实现基于事件的异步模式,您必须提供一些保证来确保类的行为正确且类的客户端能够依赖这种行为。
完成
成功完成操作、遇到错误或取消操作时,始终要调用方法名称Completed 事件处理程序。任何情况下,应用程序都不应遇到这样的情况:应用程序保持空闲状态,而操作却一直不能完成。此规则的一个例外是:异步操作本身被设计为永远不完成。
完成的事件和 EventArgs
对于每个单独的方法名称Async 方法,应用以下设计要求:
在该方法所在类中定义一个方法名称Completed 事件。
为自 AsyncCompletedEventArgs 类派生的方法名称Completed 事件定义一个 EventArgs 类和伴随的委托。默认类名称的格式应当是方法名称CompletedEventArgs。
确保 EventArgs 类是特定于方法名称方法的返回值的。当使用 EventArgs 类时,切勿要求开发人员强制转换结果。
下面的代码示例分别演示了此项设计要求的合理实现和错误实现。
[C#]
// Good design
private void Form1_MethodNameCompleted(object sender, xxxCompletedEventArgs e)
{
DemoType result = e.Result;
}
// Bad design
private void Form1_MethodNameCompleted(object sender, MethodNameCompletedEventArgs e)
{
DemoType result = (DemoType)(e.Result);
}
同时执行操作
如果类支持多个并发调用,开发人员则可以通过定义带有对象赋值状态参数或任务 ID(名为 userSuppliedState)的方法名称Async 重载,分别跟踪每个调用。此参数应当始终是方法名称Async 方法的签名中的最后一个参数。
如果类定义了带有对象赋值状态参数或任务 ID 的方法名称Async 重载,请确保使用该任务 ID 来跟踪操作的生存期,并确保将该任务 ID 返回到完成处理程序。有一些用来提供帮助的帮助器类。有关并发管理的更多信息,请参见演练:实现支持基于事件的异步模式的组件。
如果类定义了没有状态参数的方法名称Async 方法,而且它不支持多个并发调用,请确保任何在前一方法名称Async 调用完成之前调用方法名称Async 的尝试都会引发 InvalidOperationException。
一般来说,如果多次调用了不带 userSuppliedState 参数的方法名称Async 方法(会导致多个未处理的操作),则不要引发异常。如果类明显无法处理这种情况,则将引发异常,但可假定开发人员能够处理多个不可区分回调。
访问结果
如果在执行异步操作期间出现错误,其结果应当不可访问。确保在 Error 不为 null 时访问 AsyncCompletedEventArgs 中的任何属性都会引发由 Error 引用的异常。AsyncCompletedEventArgs 类为达到此目的提供了 RaiseExceptionIfNecessary 方法。
确保访问结果的任何尝试将引发 InvalidOperationException,指出该操作已被取消。请使用 AsyncCompletedEventArgs.RaiseExceptionIfNecessary 方法来进行此项验证。
进度报告
尽可能支持进度报告。这样可以使开发人员在使用您的类时能提供更好的应用程序用户体验。
如果实现了一个 ProgressChanged/方法名称ProgressChanged 事件,请确保在引发了特定异步操作的方法名称Completed 事件之后,不会引发该操作的任何此类事件。
如果正在填充标准的 ProgressChangedEventArgs,则请确保始终能够将 ProgressPercentage 解释为一个百分比。该百分比不必是一个精确值,但它应表示为百分数的形式。如果您的进度报告读数不能是一个百分数,则请从 ProgressChangedEventArgs 类中派生一个类并将 ProgressPercentage 保留为 0。避免使用非百分数的报告读数。
请确保在应用程序生命周期中的适当时间在适当的线程上引发了 ProgressChanged 事件。有关更多信息,请参见“线程处理和上下文”一节。
IsBusy 实现
如果您的类支持多个并发调用,则不要公开 IsBusy 属性。例如,XML Web services 代理不会公开 IsBusy 属性,因为它们支持异步方法的多个并发调用。
在调用方法名称Async 方法之后,但在引发方法名称Completed 事件之前,IsBusy 属性应返回 true。否则它将返回 false。BackgroundWorker 和 WebClient 组件都是公开 IsBusy 属性的类的示例。
取消
尽可能支持取消。这样可以使开发人员在使用您的类时能提供更好的应用程序用户体验。
在发生取消时,设置 AsyncCompletedEventArgs 对象中的 Cancelled 标志。
确保访问结果的任何尝试将引发 InvalidOperationException,指出该操作已被取消。请使用 AsyncCompletedEventArgs.RaiseExceptionIfNecessary 方法来进行此项验证。
请确保对取消方法发出的调用始终能够成功返回,而且从不引发异常。一般来说,客户端不会得到关于在任何给定时间是否真正取消了某个操作的通知,也不会得到关于以前发出的取消是否已经成功的通知。不过,应用程序在取消成功时总能得到通知,因为应用程序参与了完成状态。
在取消操作时,应引发 方法名称Completed 事件。
错误和异常
- 捕获所有发生在异步操作中的异常并将 AsyncCompletedEventArgs.Error 属性的值设置为该异常。
线程处理和上下文
为了使类正确运行,应当使用给定应用程序模型(包括 ASP.NET 和 Windows 窗体应用程序)的适当线程或上下文调用客户端事件处理程序,这一点很重要。我们提供了两个重要的帮助器类,以确保您的异步类在任何应用程序模型中都能正确运行,这两个帮助器类是 AsyncOperation 和 AsyncOperationManager。
AsyncOperationManager 提供了 CreateOperation 方法,该方法会返回一个 AsyncOperation。方法名称Async 方法调用 CreateOperation,类使用返回的 AsyncOperation 跟踪异步任务的生存期。
若要向客户端报告进度、增量结果和完成,请调用 AsyncOperation 的 Post 和 OperationCompleted 方法。AsyncOperation 负责将对客户端事件处理程序的调用封送到适当的线程和上下文。
说明: |
---|
如果您明确想违反应用程序模型的策略,但仍想获得使用基于事件的异步模式的其他好处,则您可以避开这些规则。例如,您可能希望在 Windows 窗体中进行操作的某个类是自由线程类。只要开发人员了解隐含的限制,您就可以创建自由线程类。控制台应用程序不会将 Post 调用的执行同步。这可导致 ProgressChanged 事件的引发顺序紊乱。如果您希望将 Post 调用的执行序列化,请实现和安装 System.Threading.SynchronizationContext 类。 |
有关使用 AsyncOperation 和 AsyncOperationManager 启用异步操作的更多信息,请参见演练:实现支持基于事件的异步模式的组件。
准则
理论上,方法调用与方法调用之间应是相互独立的。您应当避免在使用调用时使用共享资源。如果在不同调用之间共享资源,则您需要在您的实现中提供一个适当的同步机制。
建议不要进行需要客户端实现同步的设计。例如,您可以使用一个异步方法将全局静态对象作为参数来接收;这类方法的多个并发调用可能会导致数据损坏或死锁。
如果您使用多调用重载(签名中的 userState)来实现某个方法,您的类将需要管理由一系列用户状态、任务 ID 及其相应的挂起操作构成的一个集合。应当使用 lock 区域保护此集合,因为各种调用都会在此集合中添加和移除 userState 对象。
如果可能而且适合这样做,请考虑重用 CompletedEventArgs 类。在这种情况下,命名和方法名不一致,因为给定的委托和 EventArgs 类型不会仅与单独某个方法联系在一起。不过,强制开发人员强制转换从 EventArgs 上的一个属性中检索的值是绝对不可取的。
如果您正在创建自 Component 派生的类,请不要实现和安装您自己的 SynchronizationContext 类。所使用 SynchronizationContext 由应用程序模型而不是组件控制。
当您使用任何一种多线程编程时,都有可能会遇到非常严重而复杂的 Bug。在实现任何使用多线程编程的解决方案之前,请参见托管线程处理的最佳做法。