在某些情况下可能不需要使用单独的线程。如果应用程序需要定期执行简单的与 UI 有关的操作则应该考虑使用进程计时器。有时在智能客户端应用程序中使用进程计时器,以达到下列目:
|
在使用图形时保持一致的动画速度(而不管处理器的速度)
|
监视服务器和其他的应用程序以确認它们在线并且正在运行。
|
|
基于消息的通信例如,Web 服务调用和 HTTP 请求
|
|
许多因素决定了网络服务对应用程序请求的响应速度,其中包括请求的性质、网络滞后时间、连接的可靠性和带宽、单个服务或多个服务的繁忙程度
这种不可预测性可能会引起单线程应用程序的响应问題,而多线程模式是什么处理常常是一种好的解决方案应该为网络上的所有通信创建针对 UI 线程的单独线程,然后在接收到响应时将数据傳送回 UI 线程
为网络通信创建单独的线程并不总是必要的。如果应用程序通过网络进行异步通信例如使用 Microsoft Windows 消息队列(也称为 MSMQ),则在继續执行之前它不会等待响应。然而即使在这种情况下,您仍然应该使用单独的线程来侦听响应并且在响应到达时对其进行处理。
即使在处理发生在本地的情况下有些操作也可能花费很长时间,足以对应用程序的响应产生负面影响这样的操作包括:
不应该在 UI 线程上執行诸如此类的操作,因为这样做会引起应用程序中的性能问题相反,应该使用额外的线程来异步执行这些操作防止 UI 线程阻塞。
在许哆情况下也应该这样设计应用程序,让它报告正在进行的后台操作的进程和成功或失败可能还会考虑允许用户取消后台操作以提高可鼡性。
并不是应用程序必须执行的所有任务都具有相同的优先级一些任务对时间要求很急,而一些则不是在其他的情况中,您或许会發现一个线程依赖于另一个线程上的处理结果
应该创建不同优先级的线程以反映正在执行的任务的优先级。例如应该使用高优先级线程管理对时间要求很急的任务,而使用低优先级线程执行被动任务或者对时间不敏感的任务
应用程序在第一次运行时常常必须执行许多操作。例如它可能需要初始化自己的状态,检索或更新数据打开本地资源的连接。应该考虑使用单独的线程来初始化应用程序从而使得用户能够尽快地开始使用该应用程序。使用单独的线程进行初始化可以增强应用程序的响应能力和可用性
如果确实在单独的线程中執行初始化,则应该通过在初始化完成之后更新 UI 菜单和工具栏按钮的状态来防止用户启动依赖于初始化尚未完成的操作。还应该提供清楚的反馈消息来通知用户初始化的进度
在 .NET Framework 中有几种方法可以创建和使用后台线程。可以使用 ThreadPool 类访问由 .NET Framework 管理的给定进程的线程池也可以使用 Thread 类显式地创建和管理线程。另外还可以选择使用委托对象或者 Web 服务代理来使非 UI
线程上发生特定处理。本节将依次分析各种不同的方法并推荐每种方法应该在何时使用。
到现在为止您可能会认识到许多应用程序都会从多线程模式是什么处理中受益。然而线程管理並不仅仅是每次想要执行一个不同的任务就创建一个新线程的问题。有太多的线程可能会使得应用程序耗费一些不必要的系统资源特别昰,如果有大量短期运行的操作而所有这些操作都运行在单独线程上。另外显式地管理大量的线程可能是非常复杂的。
线程池化技术通过给应用程序提供由系统管理的辅助线程池解决了这些问题从而使得您可以将注意力集中在应用程序任务上而不是线程管理上。
在需偠时可以由应用程序将线程添加到线程池中。当 CLR 最初启动时线程池没有包含额外的线程。然而当应用程序请求线程时,它们就会被動态创建并存储在该池中如果线程在一段时间内没有使用,这些线程就可能会被处置因此线程池是根据应用程序的要求缩小或扩大的。
注每个进程都创建一个线程池因此,如果您在同一个进程内运行几个应用程序域则一个应用程序域中的错误可能会影响相同进程内嘚其他应用程序域,因为它们都使用相同的线程池
线程池由两种类型的线程组成:
辅助线程。辅助线程是标准系统池的一部分它们是甴 .NET Framework 管理的标准线程,大多数功能都在它们上面执行
|
|
注,如果应用程序尝试在没有 IOCompletionPorts 功能的计算机上执行 I/O 操作它就会还原到使用辅助线程。
对于每个计算机处理器线程池都默认包含 25 个线程。如果所有的 25 个线程都在被使用则附加的请求将排入队列,直到有一个线程变得可鼡为止每个线程都使用默认堆栈大小,并按默认的优先级运行
下面代码示例说明了线程池的使用。
Framework 定义了 WaitCallback 委托该委托引用的方法接受一个对象参数并且没有返回值。下面的方法实现您想要执行的代码
只要有可能,就应该使用 ThreadPool 类来创建线程然而,在一些情况下您還是需要创建并管理您自己的线程,而不是使用 ThreadPool 类
在下面的情况下,使用 Thread 对象:
需要具有特定优先级的任务
|
有可能运行很长时间的任務(这样可能阻塞其他任务)。
|
需要确保只有一个线程可以访问特定的程序集
|
需要有与线程相关的稳定标识。
|
Thread 对象包括许多属性和方法它们可以帮助控制线程。可以设置线程的优先级查询当前的线程状态,中止线程临时阻塞线程,并且执行许多其他的线程管理任务
下面的代码示例演示了如何使用 Thread 对象创建并启动一个线程。
Framework 遇到像上面一样的委托声明就隐式声明了一个从 MultiCastDelegate 类继承的隐藏类,正如下媔的代码示例中所示
委托类型 LongCalculationDelegate 用于引用接受单个整型参数并返回一个字符串的方法。下面的代码示例举例说明了一个这种类型的委托咜引用带有相关签名的特定方法。
在本示例中calculationMethod 是实现您想要在单独线程上执行的计算的方法的名称。
可以同步或异步调用委托实例所引鼡的方法为了同步调用它,可以使用下面的代码
该代码在内部使用上面的委托类型中定义的 Invoke 方法。因为 Invoke 方法是同步调用所以此方法呮在调用方法返回之后才返回。返回值是调用方法的结果
方法来启动线程上所需的操作,并且它提供 EndInvoke 方法来允许完成异步操作以及将任哬得到的数据传送回调用线程在后台处理完成之后,可以调用回调方法其中,可以调用 EndInvoke 来获取异步操作的结果
当调用 BeginInvoke 方法时,它不會等待调用完成;相反它会立即返回一个 IAsyncResult 对象,该对象可以用来监视该调用的进度可以使用 IAsyncResult 对象的 WaitHandle 成员来等待异步调用完成,或使用
IsComplete 荿员轮询是否完成如果在调用完成之前调用 EndInvoke 方法,它就会阻塞并且只在调用完成之后才返回。然而您应该慎重,不要使用这些技术來等待调用完成因为它们可能阻塞 UI 线程。一般来说回调机制是通知调用已经完成的最好方式。
异步执行委托引用的方法
异步调用 Web 服务
應用程序常常使用 Web 服务与网络资源进行通信一般来说,不应该从 UI 线程同步调用 Web 服务这是因为 Web 服务调用的响应时间变化很大,正如网络仩所有交互的响应时间的情况一样相反,应该从客户端异步调用所有的 Web 服务
要了解如何异步调用 Web 服务,可以考虑使用下面简单的 Web 服务它睡眠一段时间,然后返回一个字符串指示它已经完成了它的操作。
开发系统中引用 Web 服务时它会自动生成一个代理。代理是一个类它允许使用 .NET Framework 实现的异步调用 模式异步调用 Web 服务。如果您分析一下生成的代理您就会看到下面三个方法。
第一个方法是调用 Web 服务的同步方法第二个和第三个方法是异步方法。可以如下所示异步调用 Web 服务
这个示例非常类似于使用自定义委托的异步回调示例。当 Web 服务返回時用将要调用的方法定义一个 AsyncCallback 对象。用指定回调和代理本身的方法调用异步 Web 服务正如下面的代码示例所示:
当 Web 服务完成时,就调用完荿的回调方法然后,可以通过调用代理上的 EndReturnMessageAfterDelay 来获取异步结果
使用任务处理 UI 线程和其他线程之间的交互
设计多线程模式是什么应用程序朂复杂的一个方面是处理 UI 线程和其他线程之间的关系。用于应用程序的后台线程并不直接与应用程序 UI 交互这一点相当关键。如果后台线程试图修改应用程序的 UI
中的控件该控件就可能会处于一种未知的状态。这可能会在应用程序中引起较大的问题并难于诊断。例如当叧一个线程正在给动态生成的位图传送新数据时,它或许不能显示或者,当数据集正在刷新时绑定到数据集的组件可能会显示冲突信息。
为了避免这些问题应该从不允许 UI 线程以外的线程更改 UI 控件或绑定到 UI 的数据对象。您应该始终尽力维护 UI 代码和后台处理代码之间的严格分离
将 UI 线程与其他线程分离是一个良好的做法,但是您仍然需要在这些线程之间来回传递信息多线程模式是什么应用程序通常需要具有下列功能:
从后台线程获得结果并更新 UI。
|
当后台线程执行它的处理时向 UI 报告进度
|
从 UI 控制后台线程,例如让用户取消后台处理
|
从处悝后台线程的代码中分离 UI 代码的有效方法是,根据任务构造应用程序并且使用封装所有任务细节的对象代表每个任务。
任务是用户期望能够在应用程序内完成的一个工作单元在多线程模式是什么处理的环境中,Task 对象封装了所有的线程细节这样它们就可以从 UI 中清晰地分離出来。
通过使用 Task 模式在使用多线程模式是什么时可以简化代码。Task 模式将线程管理代码从 UI 代码中清晰地分离出来UI 使用 Task 对象提供的属性囷方法来执行行动,比如启动和停止任务、以及查询它们的状态Task 对象也可以提供许多事件,从而允许将状态信息传送回 UI这些事件都应該在 UI
线程内激发,这样UI 就不需要了解后台线程。
使用 Task 对象可以充分简化线程交互Task 对象虽然负责控制和管理后台线程,但是激发 UI 可以使鼡并且保证在 UI 线程上的事件Task 对象可以在应用程序的各个部分中重用,甚至也可以在其他的应用程序中重用
图 6.1 说明了使用 Task 模式时代码的整体结构。
注Task 模式可以用来在单独的线程上执行本地后台处理任务或者与网络上的远程服务异步交互。在后者的情况下Task 对象常常称为垺务代理。服务代理可以使用与 Task 对象相同的模式并且可以支持使其与 UI 交互更容易的属性和事件。
因为 Task 对象封装了任务的状态所以可以鼡它来更新 UI。要这样做无论何时发生更改,都可以让 Task 对象针对主 UI 线程激发 PropertyChanged 事件这些事件提供一种标准而一致的方法来传递属性值更改。
可以使用任务来通知主 UI 线程进度或其他状态改变例如,当任务变得可用时可以将其设置为已启用的标志,该标志可用于启用相应的菜单项和工具栏按钮相反,当任务变得不可用(例如因为它还在进行中),可以将已启用标志设置为 false这会导致主 UI 线程中的事件处理程序禁用适当的菜单项和工具栏按钮。
也可以使用任务来更新绑定到 UI 的数据对象应该确保数据绑定到 UI 控件的任何数据对象在 UI 线程上更新。例如如果将 DataSet 对象绑定到 UI 并从 Web 服务检索更新信息,就可以将新数据传递给 UI 代码然后,UI 代码将新数据合并到 UI 线程上绑定的 DataSet 对象中
可以使用 Task 对象实现后台处理和线程控制逻辑。因为 Task 对象封装了必要的状态和数据所以它可以协调在一个或更多线程模式是什么中完成任务所需的工作,并且在需要时传递更改和通知到应用程序的 UI可以实现所有必需的锁定和同步并将其封装在 Task 对象中,这样 UI
线程就不必处理这些問题
下面的代码示例显示了管理长期计算任务的类定义。
注虽然该示例比较简单但是它可以很容易地扩展为支持在应用程序的 UI 中集成嘚复杂后台任务。
CalculationTask 类定义一个默认的构造函数和两个公共方法来启动和停止计算它还定义了帮助器方法来帮助 Task 对象激发针对 UI 的事件。Calculate 方法实现计算逻辑并且运行在后台线程上。EndCalculate
方法实现回调方法它是在后台计算线程完成之后调用的。
CalculationStatus 成员是一个枚举它定义了在任何┅个时刻计算可能处于的三个状态。
Task 类提供两个事件:一个通知 UI 有关计算状态的事件另一个通知 UI 有关计算进度的事件。委托签名与事件夲身都要定义
这两个事件是在帮助器方法中激发的。这些方法检查目标的类型如果目标类型是从 Control 类派生的,它们就使用 Control 类中的 Invoke 方法来噭发事件因此,对于 UI 事件接收器可以保证事件是在 UI 线程上调用的。下面的示例展示了激发事件的代码
这段代码首先检查事件接收器昰否已经注册,如果它已经注册就检查目标的类型。如果目标类型是从 Control 类派生的就使用 Invoke 方法激发该事件以确保在 UI 线程上处理它。如果目标类型不是从 Control 类派生的就正常激发事件。在
FireProgressChangedEvent 方法中以相同的方式激发事件以向 UI 报告计算进度,如下列的示例所示
CalculationEventArgs 类定义了两个事件的事件参数,并且包含计算状态和进度参数以便将它们发送给 UI。CalculationEventArgs 类的定义如下所示
方法,如下面的示例所示
StopCalculation 方法负责取消计算,洳下面的代码示例所示
当调用 StopCalculation 时,计算状态被设置为 CancelPending以通知后台停止计算。向 UI 激发一个事件以通知已经接收到取消请求。
这两个方法都使用 lock 关键字来确保对计算状态变量的更改是原子的这样应用程序就不会遇到争用情形。这两个方法都激发状态改变事件来通知 UI 计算囸在启动或停止
注为清楚起见,计算的细节已经忽略
每次传递都是通过循环进行的,这样就可以检查计算状态成员以查看用户是否巳经取消了计算。如果这样循环就退出,从而完成计算方法如果计算继续进行,就使用 FireProgressChanged 帮助器方法来激发事件以向 UI 报告进度。
在计算完成之后就调用 EndCalculate 方法,以便通过调用 EndInvoke 方法来完成异步调用如下面的示例所示。
EndCalculate 将计算状态重置为 NotCalculating准备开始下一次计算。同时它噭发一个状态改变事件,这样就可以通知 UI 计算已经完成
Task 类负责管理后台线程。要使用 Task 类必须做的事情就是创建一个 Task 对象,注册它激发嘚事件并且实现这些事件的处理。因为事件是在 UI 线程上激发的所以您根本不必担心代码中的线程处理问题。
下面的示例展示了如何创建 Task 对象在这个示例中,UI 有两个按钮一个用于启动计算,一个用于停止计算还有一个进度栏显示当前的计算进度。
用于计算状态和计算进度事件的事件处理程序相应地更新 UI例如通过更新状态栏控件。
下面的代码展示的 CalculationStatusChanged 事件处理程序更新进度栏的值以反映当前的计算进喥假定进度栏的最小值和最大值已经初始化。
在这个示例中CalculationStatusChanged 事件处理程序根据计算状态启用和禁用启动和停止按钮。这可以防止用户嘗试启动一个已经在进行的计算并且向用户提供有关计算状态的反馈。
通过使用 Task 对象中的公共方法UI 为每个按钮单击实现了窗体事件处悝程序,以便启动和停止计算例如,启动按钮事件处理程序调用 StartCalculation 方法如下所示。
类似地停止计算按钮通过调用 StopCalculation 方法来停止计算,如丅所示
多线程模式是什么处理是创建可以响应的智能客户端应用程序的重要部分。应该分析多线程模式是什么适合于应用程序的什么地方并且注意在单独线程上进行不直接涉及 UI 的所有处理。在大多数情况下可以使用 ThreadPool 类创建线程。然而在某些情况下,必须使用 Thread 类来作為代替在另外一些情况下,需要使用委托对象或 Web
服务代理来使特定的处理在非 UI 线程上进行
在多线程模式是什么应用程序中,必须确保 UI 線程负责所有与 UI 有关的任务这样就可以有效地管理 UI 线程和其他线程之间的通信。Task 模式可以帮助大大简化这种交互
|