基于事件的异步编程
基于事件的异步模式具有多线程应用程序的优点,同时隐藏了多线程设计中固有的许多复杂问题。 使用支持此模式的类,你将能够:
- “在后台”执行耗时任务(例如下载和数据库操作),但不会中断你的应用程序。
- 同时执行多个操作,每个操作完成时都会接到通知。
- 等待资源变得可用,但不会停止(“阻止”)你的应用程序。
- 使用熟悉的事件和委托模型与挂起的异步操作通信。
支持基于事件的异步模式的类将具有一个或多个命名为 MethodNameAsync
的方法。 这些方法可能会创建同步版本的镜像,这些同步版本会在当前线程上执行相同的操作。 该类还可能具有 MethodNameCompleted
事件,并且可能会具有 MethodNameAsyncCancel
(或只是 CancelAsync
)方法。
PictureBox
是一个支持基于事件的异步模式的典型组件。 你可以通过调用其 Load
方法来同步下载图像,但是如果图像很大,或者网络连接很慢,应用程序将停止响应,直到下载操作完成并且对 Load
的调用返回后才会继续执行。
如果你想要应用程序在加载图像时保持运行,你可以调用 LoadAsync
方法,处理 LoadCompleted
事件,这与你处理任何其他事件一样。 调用 LoadAsync
方法时,你的应用程序将继续运行,而下载操作将在另一个线程上(“在后台”)继续。 图像加载操作完成时,将调用你的事件处理程序,并且你的事件处理程序可以检查 AsyncCompletedEventArgs
参数以确定该下载是否已成功完成。
基于事件的异步模式要求异步操作可以取消,并且 PictureBox
控件使用其 CancelAsync
方法支持此要求。 调用 CancelAsync
会提交一个停止挂起的下载的请求,任务取消时会引发 LoadCompleted
事件。
基于事件的异步模式示例
public class AsyncExample
{
// Synchronous methods.
public int Method1(string param);
public void Method2(double param);
// Asynchronous methods.
public void Method1Async(string param);
public void Method1Async(string param, object userState);
public event Method1CompletedEventHandler Method1Completed;
public void Method2Async(double param);
public void Method2Async(double param, object userState);
public event Method2CompletedEventHandler Method2Completed;
public void CancelAsync(object userState);
public bool IsBusy { get; }
// Class implementation not shown.
}
这里虚构的 AsyncExample
类有两个方法,都支持同步和异步调用。 同步重载的行为类似于方法调用,它们对调用线程执行操作;如果操作很耗时,则调用的返回可能会有明显的延迟。 异步重载将在另一个线程上启动操作,然后立即返回,允许在调用线程继续执行的同时让操作“在后台”执行。
异步方法重载
异步操作可以有两个重载:单调用和多调用。 你可以通过方法签名来区分这两种形式:多调用形式有一个额外的参数,即 userState
。 使用这种形式,你的代码可以多次调用 Method1Async(string param, object userState)
,而不必等待任何挂起的异步操作的完成。 另一方面,如果你尝试在前一个调用尚未完成时调用 Method1Async(string param)
,该方法将引发 InvalidOperationException
。
多调用重载的 userState
参数可帮助你区分各个异步操作。 应分别为各个 Method1Async(string param, object userState)
调用提供唯一值(例如,GUID 或哈希代码);这样,当各个操作完成时,事件处理程序便可以确定是哪个操作实例抛出了完成事件。
跟踪挂起的操作
如果你使用多调用重载,你的代码将需要跟踪挂起任务的 userState
对象(任务 ID)。 对于各个 Method1Async(string param, object userState)
调用,通常会生成新的唯一 userState
对象,并将它添加到集合中。 当对应于此 userState
对象的任务引发完成事件时,你的完成方法实现将检查 AsyncCompletedEventArgs.UserState
并将此对象从集合中删除。 在以这种方式使用时,userState
参数充当任务 ID 的角色。
在为你对多调用重载的调用中的
userState
提供唯一值时,一定要小心。 如果任务 ID 不唯一,将导致异步类引发ArgumentException
。
取消挂起的操作
我们必须能够在异步操作完成之前随时取消它们,这一点很重要。 实现基于事件的异步模式的类包含 CancelAsync
方法(如果只有一个异步方法),或 MethodNameAsyncCancel
方法(如果有多个异步方法)。
允许多个调用采用 userState
参数的方法,此类方法可用于跟踪每个任务的生存期。 CancelAsync
采用 userState
参数,该参数可用于取消特定挂起任务。
一次只支持一个挂起操作的方法(如 Method1Async(string param)
)是不可取消的。
接收进度更新和增量结果
符合基于事件的异步模式的类可以为跟踪进度和增量结果提供事件。 此事件通常命名为 ProgressChanged
或 MethodNameProgressChanged
,其对应的事件处理程序将使用 ProgressChangedEventArgs
参数。
ProgressChanged
事件的事件处理程序可以检查 ProgressChangedEventArgs.ProgressPercentage
属性,以确定异步任务完成百分比。 此属性的范围是 0 到 100,可用来更新 ProgressBar.Value
属性。 如果有多个异步操作挂起,你可以使用 ProgressChangedEventArgs.UserState
属性来分辨出哪个操作在报告进度。
实现基于事件的异步模式的最佳做法
若要实现基于事件的异步模式,你必须提供一些保证来确保类的行为正确且类的客户端能够依赖这种行为。
完成
操作成功完成、出错或取消时,始终应调用 MethodNameCompleted
事件处理程序 。 任何情况下,应用程序都不应遇到这样的情况:应用程序保持空闲状态,而操作却一直不能完成。 此规则的例外情况是:异步操作本身设计为永不完成。
已完成的事件和EventArgs
针对每个单独的 MethodNameAsync
方法,请应用以下设计要求:
- 在与该方法相同的类上定义
MethodNameCompleted
事件。 - 为
MethodNameCompleted
事件定义一个AsyncCompletedEventArgs
,默认类名应采用MethodNameCompletedEventArgs
形式。 - 确保
EventArgs
类特定于MethodName
方法的返回值。 在使用EventArgs
类时,切勿要求开发人员强制转换结果。 - 不要为返回
void
的方法定义EventArgs
,而应使用AsyncCompletedEventArgs
类的实例。 - 应务必始终抛出
MethodNameCompleted
事件。 成功完成、出错或者取消时应引发此事件。 任何情况下,应用程序都不应遇到这样的情况:应用程序保持空闲状态,而操作却一直不能完成。 - 确保可以捕获异步操作中发生的任何异常并将捕获的异常分配给
Error
属性。 - 如果完成任务时出现错误,其结果应当不可访问。 当
Error
属性不为null
时,确保访问EventArgs
结构中的任何属性都会引发异常。 使用 RaiseExceptionIfNecessary 方法来执行此验证。 - 如果发生任何异常,应抛出 MethodNameCompleted 事件,并将该异常分配给
AsyncCompletedEventArgs.Error
属性。 - 如果类支持多个并发调用,应确保 MethodNameCompleted 事件包含相应的 userSuppliedState 对象。
- 应确保在应用生命周期中适时对相应线程抛出 MethodNameCompleted 事件。
线程处理和上下文
为了使类正确运行,应当使用给定应用程序模型(包括 ASP.NET 和 Windows 窗体应用程序)的适当线程或上下文调用客户端事件处理程序,这一点很重要。 我们提供了两个重要的帮助器类,以确保你的异步类在任何应用程序模型中都能正确运行,这两个帮助器类是 AsyncOperation 和 AsyncOperationManager。
AsyncOperationManager
提供了 CreateOperation
方法,该方法会返回一个 AsyncOperation
。 MethodNameAsync
方法调用 CreateOperation
,且类使用返回的 AsyncOperation
跟踪异步任务的生存期。
若要将进程、增量结果和完成情况报告给客户端,请调用 AsyncOperation
上的 Post
和 OperationCompleted
方法。 AsyncOperation
负责将对客户端事件处理程序的调用封送到合适的线程或上下文。