您的位置:首页 > 编程语言 > C#

C#---------多线程

2014-08-29 16:54 190 查看

前台线程和后台线程之间的选择

.NET Framework 中的所有线程都被指定为前台线程或后台线程。这两种线程唯一的区别是 — 后台线程不会阻止进程终止。在属于一个进程的所有前台线程终止之后,公共语言运行库 (CLR) 就会结束进程,从而终止仍在运行的任何后台线程。在默认情况下,通过创建并启动新的 Thread 对象生成的所有线程都是前台线程,而从非托管代码进入托管执行环境中的所有线程都标记为后台线程。然而,通过修改 Thread.IsBackground 属性,可以指定一个线程是前台线程还是后台线程。通过将 Thread.IsBackground 设置为 true,可以将一个线程指定为后台线程;通过将 Thread.IsBackground 设置为 false,可以将一个线程指定为前台线程。有关 Thread 对象的详细信息,请参阅本章后面的“使用 Thread 类”部分在大多数应用程序中,您会选择将不同的线程设置成前台线程或后台线程。通常,应该将被动侦听活动的线程设置为后台线程,而将负责发送数据的线程设置为前台线程,这样,在所有的数据发送完毕之前该线程不会被终止。只有在确认线程被系统随意终止没有不利影响时,才应该使用后台线程。如果线程正在执行必须完成的敏感操作或事务操作,或者需要控制关闭线程的方式以便释放重要资源,则使用前台线程。

处理锁定和同步

有时在构建应用程序时,创建的多个线程都需要同时使用关键资源(例如数据或应用程序组件)。如果不仔细,一个线程就可能更改另一个线程正在使用的资源。其结果可能就是该资源处于一种不确定的状态并且呈现为不可用。这称为 争用情形。在没有仔细考虑共享资源使用的情况下使用多线程的其他不利影响包括:死锁、线程饥饿和线程关系问题。为了防止这些影响,当从两个或多个线程访问一个资源时,需要使用锁定和同步技术来协调这些尝试访问此资源的线程。使用锁定和同步来管理线程访问共享资源是一项复杂的任务,只要有可能,就应该通过在线程之间传送数据而不是提供对单个实例的共享访问来避免这样做。假如不能排除线程之间的资源共享,则应该:使用 Microsoft Visual C# 中的 lock 语句和 Microsoft Visual Basic .NET 中的 SyncLock 语句来创建临界区,但要小心地从临界区内调用方法来防止死锁。
使用 Synchronized 方法获得线程安全的 .NET 集合。
使用 ThreadStatic 属性创建逐线程成员。
使用重新检查 (double-check) 锁或 Interlocked.CompareExchange 方法来防止不必要的锁定。
确保静态声明是线程安全的。
有关锁定和同步技术的详细信息,请参阅 http://msdn.microsoft.com/library/en-us/cpgenref/html/cpconthreadingdesignguidelines.asp 上的 .NET Framework General Reference 中的“Threading Design Guidelines”。

使用计时器

在某些情况下,可能不需要使用单独的线程。如果应用程序需要定期执行简单的与 UI 有关的操作,则应该考虑使用进程计时器。有时,在智能客户端应用程序中使用进程计时器,以达到下列目:按计划定期执行操作。
在使用图形时保持一致的动画速度(而不管处理器的速度)。
监视服务器和其他的应用程序以确认它们在线并且正在运行。
.NET Framework 提供三种进程计时器:System.Window.Forms.Timer
System.Timers.Timer
System.Threading.Timer
如果想要在 Windows 窗体应用程序中引发事件,System.Window.Forms.Timer 就非常有用。它经过了专门的优化以便与 Windows 窗体一起使用,并且必须用在 Windows 窗体中。它设计成能用于单线程环境,并且可以在 UI 线程上同步操作。这就意味着该计时器从来不会抢占应用程序代码的执行(假定没有调用Application.DoEvents),并且对与 UI 交互是安全的。System.Timers.Timer 被设计并优化成能用于多线程环境。与 System.Window.Forms.Timer 不同,此计时器调用从 CLR 线程池中获得的辅助线程上的事件处理程序。在这种情况下,应该确保事件处理程序不与 UI 交互。System.Timers.Timer 公开了可以模拟 System.Windows.Forms.Timer 中的行为的SynchronizingObject 属性,但是除非需要对事件的时间安排进行更精确的控制,否则还是应该改为使用 System.Windows.Forms.TimerSystem.Threading.Timer 是一个简单的轻量级服务器端计时器。它并不是内在线程安全的,并且使用起来比其他计时器更麻烦。此计时器通常不适合 Windows 窗体环境。表 6.1 列出了每个计时器的各种属性。 6.1 进程计时器属性
属性System.Windows.FormsSystem.TimersSystem.Threading
计时器事件运行在什么线程中?UI 线程UI 线程或辅助线程辅助线程
实例是线程安全的吗?
需要 Windows 窗体吗?
最初的计时器事件可以调度吗?

何时使用多线程

在许多常见的情况下,可以使用多线程处理来显著提高应用程序的响应能力和可用性。应该慎重考虑使用多线程来:通过网络(例如,与 Web 服务器、数据库或远程对象)进行通信。
执行需要较长时间因而可能导致 UI 冻结的本地操作。
区分各种优先级的任务。
提高应用程序启动和初始化的性能。
非常详细地分析这些使用情况是非常有用的。

通过网络进行通信

智能客户端可以采用许多方式通过网络进行通信,其中包括:远程对象调用,例如,DCOM、RPC 或 .NET 远程处理
基于消息的通信,例如,Web 服务调用和 HTTP 请求。
分布式事务处理。
许多因素决定了网络服务对应用程序请求的响应速度,其中包括请求的性质、网络滞后时间、连接的可靠性和带宽、单个服务或多个服务的繁忙程度。这种不可预测性可能会引起单线程应用程序的响应问题,而多线程处理常常是一种好的解决方案。应该为网络上的所有通信创建针对 UI 线程的单独线程,然后在接收到响应时将数据传送回 UI 线程。为网络通信创建单独的线程并不总是必要的。如果应用程序通过网络进行异步通信,例如使用 Microsoft Windows 消息队列(也称为 MSMQ),则在继续执行之前,它不会等待响应。然而,即使在这种情况下,您仍然应该使用单独的线程来侦听响应,并且在响应到达时对其进行处理。

执行本地操作

即使在处理发生在本地的情况下,有些操作也可能花费很长时间,足以对应用程序的响应产生负面影响。这样的操作包括:图像呈现。
数据操纵。
数据排序。
搜索。
不应该在 UI 线程上执行诸如此类的操作,因为这样做会引起应用程序中的性能问题。相反,应该使用额外的线程来异步执行这些操作,防止 UI 线程阻塞。在许多情况下,也应该这样设计应用程序,让它报告正在进行的后台操作的进程和成功或失败。可能还会考虑允许用户取消后台操作以提高可用性。

区分各种优先级的任务

并不是应用程序必须执行的所有任务都具有相同的优先级。一些任务对时间要求很急,而一些则不是。在其他的情况中,您或许会发现一个线程依赖于另一个线程上的处理结果。应该创建不同优先级的线程以反映正在执行的任务的优先级。例如,应该使用高优先级线程管理对时间要求很急的任务,而使用低优先级线程执行被动任务或者对时间不敏感的任务。

应用程序启动

应用程序在第一次运行时常常必须执行许多操作。例如,它可能需要初始化自己的状态,检索或更新数据,打开本地资源的连接。应该考虑使用单独的线程来初始化应用程序,从而使得用户能够尽快地开始使用该应用程序。使用单独的线程进行初始化可以增强应用程序的响应能力和可用性。如果确实在单独的线程中执行初始化,则应该通过在初始化完成之后,更新 UI 菜单和工具栏按钮的状态来防止用户启动依赖于初始化尚未完成的操作。还应该提供清楚的反馈消息来通知用户初始化的进度。

返回页首

创建和使用线程

在 .NET Framework 中有几种方法可以创建和使用后台线程。可以使用 ThreadPool 类访问由 .NET Framework 管理的给定进程的线程池,也可以使用 Thread 类显式地创建和管理线程。另外,还可以选择使用委托对象或者 Web 服务代理来使非 UI 线程上发生特定处理。本节将依次分析各种不同的方法,并推荐每种方法应该在何时使用。

使用 ThreadPool 类

到现在为止,您可能会认识到许多应用程序都会从多线程处理中受益。然而,线程管理并不仅仅是每次想要执行一个不同的任务就创建一个新线程的问题。有太多的线程可能会使得应用程序耗费一些不必要的系统资源,特别是,如果有大量短期运行的操作,而所有这些操作都运行在单独线程上。另外,显式地管理大量的线程可能是非常复杂的。线程池化技术通过给应用程序提供由系统管理的辅助线程池解决了这些问题,从而使得您可以将注意力集中在应用程序任务上而不是线程管理上。在需要时,可以由应用程序将线程添加到线程池中。当 CLR 最初启动时,线程池没有包含额外的线程。然而,当应用程序请求线程时,它们就会被动态创建并存储在该池中。如果线程在一段时间内没有使用,这些线程就可能会被处置,因此线程池是根据应用程序的要求缩小或扩大的。每个进程都创建一个线程池,因此,如果您在同一个进程内运行几个应用程序域,则一个应用程序域中的错误可能会影响相同进程内的其他应用程序域,因为它们都使用相同的线程池。线程池由两种类型的线程组成:辅助线程。辅助线程是标准系统池的一部分。它们是由 .NET Framework 管理的标准线程,大多数功能都在它们上面执行。
完成端口线程.这种线程用于异步 I/O 操作(通过使用 IOCompletionPorts API)。
,如果应用程序尝试在没有 IOCompletionPorts 功能的计算机上执行 I/O 操作,它就会还原到使用辅助线程。对于每个计算机处理器,线程池都默认包含 25 个线程。如果所有的 25 个线程都在被使用,则附加的请求将排入队列,直到有一个线程变得可用为止。每个线程都使用默认堆栈大小,并按默认的优先级运行。在下面的情况下,使用 ThreadPool 类:有大量小的独立任务要在后台执行。
不需要对用来执行任务的线程进行精细控制。

使用 Thread 类

使用 Thread 类可以显式管理线程。这包括 CLR 创建的线程和进入托管环境执行代码的 CLR 以外创建的线程。CLR 监视其进程中曾经在 .NET Framework 内执行代码的所有线程,并且使用 Thread 类的实例来管理它们。只要有可能,就应该使用 ThreadPool 类来创建线程。然而,在一些情况下,您还是需要创建并管理您自己的线程,而不是使用 ThreadPool 类。在下面的情况下,使用 Thread 对象:需要具有特定优先级的任务。
有可能运行很长时间的任务(这样可能阻塞其他任务)。
需要确保只有一个线程可以访问特定的程序集。
需要有与线程相关的稳定标识。
Thread 对象包括许多属性和方法,它们可以帮助控制线程。可以设置线程的优先级,查询当前的线程状态,中止线程,临时阻塞线程,并且执行许多其他的线程管理任务。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息