第 6 章 — 使用多线程
线程是基本执行单元。单线程执行一系列应用程序指令,并且在应用程序中从头到尾都经由单一的逻辑路径。所有的应用程序都至少有一个线程,但是您可以将它们设计成使用多线程,并且每个线程执行一个单独的逻辑。在应用程序中使用多线程,可以将冗长的或非常耗时的任务放在后台处理。即使在只有单处理器的计算机上,使用多线程也可以非常显著地提高应用程序的响应能力和可用性。
使用多线程来开发应用程序可能非常复杂,特别是当您没有仔细考虑锁定和同步问题时。当开发智能客户端应用程序时,需要仔细地评估应该在何处使用多线程和如何使用多线程,这样就可以获得最大的好处,而无需创建不必要的复杂并难于调试的应用程序。
本章研究对于开发多线程智能客户端应用程序最重要的一些概念。它介绍了一些值得推荐的在智能客户端应用程序中使用多线程的方法,并且描述了如何实现这些功能。
.NET Framework 中的多线程处理
所有的 .NET Framework 应用程序都是使用单线程创建的,单线程用于执行该应用程序。在智能客户端应用程序中,这样的线程创建并管理用户界面
(UI),因而称为 UI 线程。
可以将 UI 线程用于所有的处理,其中包括 Web 服务调用、远程对象调用和数据库调用。然而,以这种方式使用
UI 线程通常并不是 一个好主意。在大多数情况下,您不能预测调用 Web 服务、远程对象或数据库会持续多久,而且在
UI 线程等待响应时,您可能会导致 UI 冻结。
通过创建附加线程,应用程序可以在不使用 UI 线程的情况下执行额外的处理。当应用程序调用 Web 服务时,可以使用多线程来防止
UI 冻结或并行执行某些本地任务,以整体提高应用程序的效率。在大多数情况下,您应该坚持在单独的线程上执行任何与
UI 无关的任务。
同步和异步调用之间的选择
应用程序既可以进行同步调用,也可以进行异步调用。同步 调用在继续之前等待响应或返回值。如果不允许调用继续,就说调用被阻塞
了。
异步 或非阻塞 调用不等待响应。异步调用是通过使用单独的线程执行的。原始线程启动异步调用,异步调用使用另一个线程执行请求,而与此同时原始的线程继续处理。
对于智能客户端应用程序,将 UI 线程中的同步调用减到最少非常重要。在设计智能客户端应用程序时,应该考虑应用程序将进行的每个调用,并确定同步调用是否会对应用程序的响应和性能产生负面影响。
仅在下列情况下,使用 UI 线程中的同步调用:
执行操纵 UI 的操作。
执行不会产生导致 UI 冻结的风险的小的、定义完善的操作。
在下列情况下,使用 UI 线程中的异步调用:
执行不影响 UI 的后台操作。
调用位于网络的其他系统或资源。
执行可能花费很长时间才能完成的操作。
前台线程和后台线程之间的选择
.NET Framework 中的所有线程都被指定为前台线程或后台线程。这两种线程唯一的区别是 — 后台线程不会阻止进程终止。在属于一个进程的所有前台线程终止之后,公共语言运行库
(CLR) 就会结束进程,从而终止仍在运行的任何后台线程。
在默认情况下,通过创建并启动新的 Thread 对象生成的所有线程都是前台线程,而从非托管代码进入托管执行环境中的所有线程都标记为后台线程。然而,通过修改
Thread.IsBackground 属性,可以指定一个线程是前台线程还是后台线程。通过将 Thread.IsBackground
设置为 true,可以将一个线程指定为后台线程;通过将 Thread.IsBackground 设置为 false,可以将一个线程指定为前台线程。
注有关 Thread 对象的详细信息,请参阅本章后面的“使用 Thread 类”部分。
在大多数应用程序中,您会选择将不同的线程设置成前台线程或后台线程。通常,应该将被动侦听活动的线程设置为后台线程,而将负责发送数据的线程设置为前台线程,这样,在所有的数据发送完毕之前该线程不会被终止。
只有在确认线程被系统随意终止没有不利影响时,才应该使用后台线程。如果线程正在执行必须完成的敏感操作或事务操作,或者需要控制关闭线程的方式以便释放重要资源,则使用前台线程。
处理锁定和同步
有时在构建应用程序时,创建的多个线程都需要同时使用关键资源(例如数据或应用程序组件)。如果不仔细,一个线程就可能更改另一个线程正在使用的资源。其结果可能就是该资源处于一种不确定的状态并且呈现为不可用。这称为
争用情形。在没有仔细考虑共享资源使用的情况下使用多线程的其他不利影响包括:死锁、线程饥饿和线程关系问题。
为了防止这些影响,当从两个或多个线程访问一个资源时,需要使用锁定和同步技术来协调这些尝试访问此资源的线程。
使用锁定和同步来管理线程访问共享资源是一项复杂的任务,只要有可能,就应该通过在线程之间传送数据而不是提供对单个实例的共享访问来避免这样做。
何时使用多线程
在许多常见的情况下,可以使用多线程处理来显著提高应用程序的响应能力和可用性。
应该慎重考虑使用多线程来:
? 通过网络(例如,与 Web 服务器、数据库或远程对象)进行通信。
? 执行需要较长时间因而可能导致 UI 冻结的本地操作。
? 区分各种优先级的任务。
? 提高应用程序启动和初始化的性能。
非常详细地分析这些使用情况是非常有用的。
通过网络进行通信
智能客户端可以采用许多方式通过网络进行通信,其中包括:
? 远程对象调用,例如,DCOM、RPC 或 .NET 远程处理
? 基于消息的通信,例如,Web 服务调用和 HTTP 请求。
? 分布式事务处理。
许多因素决定了网络服务对应用程序请求的响应速度,其中包括请求的性质、网络滞后时间、连接的可靠性和带宽、单个服务或多个服务的繁忙程度。
这种不可预测性可能会引起单线程应用程序的响应问题,而多线程处理常常是一种好的解决方案。应该为网络上的所有通信创建针对
UI 线程的单独线程,然后在接收到响应时将数据传送回 UI 线程。
为网络通信创建单独的线程并不总是必要的。如果应用程序通过网络进行异步通信,例如使用 Microsoft Windows
消息队列(也称为 MSMQ),则在继续执行之前,它不会等待响应。然而,即使在这种情况下,您仍然应该使用单独的线程来侦听响应,并且在响应到达时对其进行处理。
区分各种优先级的任务
并不是应用程序必须执行的所有任务都具有相同的优先级。一些任务对时间要求很急,而一些则不是。在其他的情况中,您或许会发现一个线程依赖于另一个线程上的处理结果。
应该创建不同优先级的线程以反映正在执行的任务的优先级。例如,应该使用高优先级线程管理对时间要求很急的任务,而使用低优先级线程执行被动任务或者对时间不敏感的任务。
应用程序启动
应用程序在第一次运行时常常必须执行许多操作。例如,它可能需要初始化自己的状态,检索或更新数据,打开本地资源的连接。应该考虑使用单独的线程来初始化应用程序,从而使得用户能够尽快地开始使用该应用程序。使用单独的线程进行初始化可以增强应用程序的响应能力和可用性。
如果确实在单独的线程中执行初始化,则应该通过在初始化完成之后,更新 UI 菜单和工具栏按钮的状态来防止用户启动依赖于初始化尚未完成的操作。还应该提供清楚的反馈消息来通知用户初始化的进度。
|