那些同时执行多项任务、但仍能响应用户交互的应用程序通常需要实施一种使用多线程的设计方案。 System.Threading 命名空间提供了创建高性能多线程应用程序所必需的所有工具,但要想有效地使用这些工具,需要有丰富的使用多线程软件工程的经验。 对于相对简单的多线程应用程序,BackgroundWorker 组件提供了一个简单的解决方案。 对于更复杂的异步应用程序,请考虑实现一个符合基于事件的异步模式的类。
基于事件的异步模式具有多线程应用程序的优点,同时隐匿了多线程设计中固有的许多复杂问题。 使用支持此模式的类,您将能够:
-
“在后台”执行耗时任务(例如下载和数据库操作),但不会中断您的应用程序。
-
同时执行多个操作,每个操作完成时都会接到通知。
-
等待资源变得可用,但不会停止(“挂起”)您的应用程序。
-
使用熟悉的事件和委托模型与挂起的异步操作通信。 有关使用事件处理程序和委托的更多信息,请参见 事件和委托。
支持基于事件的异步模式的类将有一个或多个名为 方法名称Async 的方法。 这些方法可能会创建同步版本的镜像,这些同步版本会在当前线程上执行相同的操作。 该类还可能有一个 方法名称Completed 事件,而且它可能会有一个 方法名称AsyncCancel(或只是 CancelAsync)方法。
PictureBox 是一个支持基于事件的异步模式的典型组件。 您可以通过调用其 Load 方法来同步下载图像,但是如果图像很大,或者网络连接很慢,您的应用程序将停止(“挂起”),直到下载操作完成并且对 Load 的调用返回后才会继续执行。
如果您希望您的应用程序在加载图像时保持运行,您可以调用 LoadAsync 方法,处理 LoadCompleted 事件,这与您处理任何其他事件没有什么两样。 调用LoadAsync 方法时,您的应用程序将继续运行,而下载操作将在另一个线程上(“在后台”)继续。 图像加载操作完成时,将会调用您的事件处理程序,您的事件处理程序可以检查 AsyncCompletedEventArgs 参数以确定下载是否已成功完成。
基于事件的异步模式要求异步操作可以取消,PictureBox 控件使用其 CancelAsync 方法来支持此要求。 调用 CancelAsync 会提交一个停止挂起的下载的请求,任务取消时会引发 LoadCompleted 事件。
警告 |
---|
下载有可能恰在发出 CancelAsync 请求时完成,因此 Cancelled 可能没有反映取消请求。 这叫做“争用条件”,是多线程编程中常见的一个问题。 有关多线程编程中的问题的更多信息,请参见 托管线程处理的最佳做法。 |
基于事件的异步模式可以采用多种形式,具体取决于某个特定类支持的操作的复杂程度。 最简单的类可能只有一个方法名称Async 方法和一个对应的方法名称Completed 事件。 更复杂的类可能有若干个方法名称Async 方法(每种方法都有一个对应的方法名称Completed 事件),以及这些方法的同步版本。 这些类分别支持各种异步方法的取消、进度报告和增量结果。
异步方法可能还支持多个挂起的调用(多个并发调用),允许您的代码在此方法完成其他挂起的操作之前调用此方法任意多次。 若要正确处理此种情况,必须让您的应用程序能够跟踪各个操作的完成。
基于事件的异步模式示例
SoundPlayer 和 PictureBox 组件表示基于事件的异步模式的简单实现。 WebClient 和 BackgroundWorker 组件表示基于事件的异步模式的更复杂的实现。
下面是一个符合此模式的类声明示例:
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 方法(如果只有一个异步方法),或有一个 方法名称AsyncCancel 方法(如果有多个异步方法)。
允许多个调用采用 userState 参数的方法,此类方法可用于跟踪每个任务的生存期。 CancelAsync 采用 userState 参数,该参数可用于取消特定挂起任务。
一次只支持一个挂起的操作的方法(如 Method1Async(string param))是不可取消的。
接收进度更新和增量结果
符合基于事件的异步模式的类可以为跟踪进度和增量结果提供事件。 此事件通常将叫做 ProgressChanged 或 方法名称ProgressChanged,它对应的事件处理程序会带有一个 ProgressChangedEventArgs 参数。
ProgressChanged 事件的事件处理程序可以检查 ProgressChangedEventArgs.ProgressPercentage 属性来确定异步任务完成的百分比。 此属性的范围是 0 到 100,可用来更新 ProgressBar 的 Value 属性。 如果有多个异步操作挂起,您可以使用 ProgressChangedEventArgs.UserState 属性来分辨出哪个操作在报告进度。
一些类可能会在异步操作继续时报告增量结果。 这些结果将保存的派生自 ProgressChangedEventArgs 的类中,并显示为此派生类中的属性。 您可以在ProgressChanged 事件的事件处理程序中访问这些结果,就像访问 ProgressPercentage 属性一样。 如果有多个异步操作挂起,您可以使用 UserState 属性来分辨出哪个操作在报告增量结果。