移动开发.docx

上传人:b****5 文档编号:4704601 上传时间:2022-12-07 格式:DOCX 页数:22 大小:43.84KB
下载 相关 举报
移动开发.docx_第1页
第1页 / 共22页
移动开发.docx_第2页
第2页 / 共22页
移动开发.docx_第3页
第3页 / 共22页
移动开发.docx_第4页
第4页 / 共22页
移动开发.docx_第5页
第5页 / 共22页
点击查看更多>>
下载资源
资源描述

移动开发.docx

《移动开发.docx》由会员分享,可在线阅读,更多相关《移动开发.docx(22页珍藏版)》请在冰豆网上搜索。

移动开发.docx

移动开发

第6章—使用多线程

摘要:

本章讨论与智能客户端应用程序中多线程的使用有关的问题。

为了最大限度地提高智能客户端应用程序的响应能力,需要仔细考虑如何和何时使用多线程。

线程可以大大提高应用程序的可用性和性能,但是当您确定它们将如何与用户界面交互时,需要对其进行非常仔细的考虑。

本页内容

.NETFramework中的多线程处理

何时使用多线程

创建和使用线程

使用任务处理UI线程和其他线程之间的交互

小结

线程是基本执行单元。

单线程执行一系列应用程序指令,并且在应用程序中从头到尾都经由单一的逻辑路径。

所有的应用程序都至少有一个线程,但是您可以将它们设计成使用多线程,并且每个线程执行一个单独的逻辑。

在应用程序中使用多线程,可以将冗长的或非常耗时的任务放在后台处理。

即使在只有单处理器的计算机上,使用多线程也可以非常显著地提高应用程序的响应能力和可用性。

使用多线程来开发应用程序可能非常复杂,特别是当您没有仔细考虑锁定和同步问题时。

当开发智能客户端应用程序时,需要仔细地评估应该在何处使用多线程和如何使用多线程,这样就可以获得最大的好处,而无需创建不必要的复杂并难于调试的应用程序。

本章研究对于开发多线程智能客户端应用程序最重要的一些概念。

它介绍了一些值得推荐的在智能客户端应用程序中使用多线程的方法,并且描述了如何实现这些功能。

.NETFramework中的多线程处理

所有的.NETFramework应用程序都是使用单线程创建的,单线程用于执行该应用程序。

在智能客户端应用程序中,这样的线程创建并管理用户界面(UI),因而称为UI线程。

可以将UI线程用于所有的处理,其中包括Web服务调用、远程对象调用和数据库调用。

然而,以这种方式使用UI线程通常并不是一个好主意。

在大多数情况下,您不能预测调用Web服务、远程对象或数据库会持续多久,而且在UI线程等待响应时,您可能会导致UI冻结。

通过创建附加线程,应用程序可以在不使用UI线程的情况下执行额外的处理。

当应用程序调用Web服务时,可以使用多线程来防止UI冻结或并行执行某些本地任务,以整体提高应用程序的效率。

在大多数情况下,您应该坚持在单独的线程上执行任何与UI无关的任务。

同步和异步调用之间的选择

应用程序既可以进行同步调用,也可以进行异步调用。

同步调用在继续之前等待响应或返回值。

如果不允许调用继续,就说调用被阻塞了。

异步或非阻塞调用不等待响应。

异步调用是通过使用单独的线程执行的。

原始线程启动异步调用,异步调用使用另一个线程执行请求,而与此同时原始的线程继续处理。

对于智能客户端应用程序,将UI线程中的同步调用减到最少非常重要。

在设计智能客户端应用程序时,应该考虑应用程序将进行的每个调用,并确定同步调用是否会对应用程序的响应和性能产生负面影响。

仅在下列情况下,使用UI线程中的同步调用:

∙执行操纵UI的操作。

∙执行不会产生导致UI冻结的风险的小的、定义完善的操作。

在下列情况下,使用UI线程中的异步调用:

∙执行不影响UI的后台操作。

∙调用位于网络的其他系统或资源。

∙执行可能花费很长时间才能完成的操作。

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

.NETFramework中的所有线程都被指定为前台线程或后台线程。

这两种线程唯一的区别是—后台线程不会阻止进程终止。

在属于一个进程的所有前台线程终止之后,公共语言运行库(CLR)就会结束进程,从而终止仍在运行的任何后台线程。

在默认情况下,通过创建并启动新的Thread对象生成的所有线程都是前台线程,而从非托管代码进入托管执行环境中的所有线程都标记为后台线程。

然而,通过修改Thread.IsBackground属性,可以指定一个线程是前台线程还是后台线程。

通过将Thread.IsBackground设置为true,可以将一个线程指定为后台线程;通过将Thread.IsBackground设置为false,可以将一个线程指定为前台线程。

注有关Thread对象的详细信息,请参阅本章后面的“使用Thread类”部分。

在大多数应用程序中,您会选择将不同的线程设置成前台线程或后台线程。

通常,应该将被动侦听活动的线程设置为后台线程,而将负责发送数据的线程设置为前台线程,这样,在所有的数据发送完毕之前该线程不会被终止。

只有在确认线程被系统随意终止没有不利影响时,才应该使用后台线程。

如果线程正在执行必须完成的敏感操作或事务操作,或者需要控制关闭线程的方式以便释放重要资源,则使用前台线程。

处理锁定和同步

有时在构建应用程序时,创建的多个线程都需要同时使用关键资源(例如数据或应用程序组件)。

如果不仔细,一个线程就可能更改另一个线程正在使用的资源。

其结果可能就是该资源处于一种不确定的状态并且呈现为不可用。

这称为争用情形。

在没有仔细考虑共享资源使用的情况下使用多线程的其他不利影响包括:

死锁、线程饥饿和线程关系问题。

为了防止这些影响,当从两个或多个线程访问一个资源时,需要使用锁定和同步技术来协调这些尝试访问此资源的线程。

使用锁定和同步来管理线程访问共享资源是一项复杂的任务,只要有可能,就应该通过在线程之间传送数据而不是提供对单个实例的共享访问来避免这样做。

假如不能排除线程之间的资源共享,则应该:

∙使用MicrosoftVisualC#中的lock语句和MicrosoftVisualBasic.NET中的SyncLock语句来创建临界区,但要小心地从临界区内调用方法来防止死锁。

∙使用Synchronized方法获得线程安全的.NET集合。

∙使用ThreadStatic属性创建逐线程成员。

∙使用重新检查(double-check)锁或Interlocked.CompareExchange方法来防止不必要的锁定。

∙确保静态声明是线程安全的。

有关锁定和同步技术的详细信息,请参阅上的.NETFrameworkGeneralReference中的“ThreadingDesignGuidelines”。

使用计时器

在某些情况下,可能不需要使用单独的线程。

如果应用程序需要定期执行简单的与UI有关的操作,则应该考虑使用进程计时器。

有时,在智能客户端应用程序中使用进程计时器,以达到下列目:

∙按计划定期执行操作。

∙在使用图形时保持一致的动画速度(而不管处理器的速度)。

∙监视服务器和其他的应用程序以确认它们在线并且正在运行。

.NETFramework提供三种进程计时器:

∙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.Timer。

System.Threading.Timer是一个简单的轻量级服务器端计时器。

它并不是内在线程安全的,并且使用起来比其他计时器更麻烦。

此计时器通常不适合Windows窗体环境。

表6.1列出了每个计时器的各种属性。

表6.1进程计时器属性

属性

System.Windows.Forms

System.Timers

System.Threading

计时器事件运行在什么线程中?

UI线程

UI线程或辅助线程

辅助线程

实例是线程安全的吗?

需要Windows窗体吗?

最初的计时器事件可以调度吗?

返回页首

何时使用多线程

在许多常见的情况下,可以使用多线程处理来显著提高应用程序的响应能力和可用性。

应该慎重考虑使用多线程来:

∙通过网络(例如,与Web服务器、数据库或远程对象)进行通信。

∙执行需要较长时间因而可能导致UI冻结的本地操作。

∙区分各种优先级的任务。

∙提高应用程序启动和初始化的性能。

非常详细地分析这些使用情况是非常有用的。

通过网络进行通信

智能客户端可以采用许多方式通过网络进行通信,其中包括:

∙远程对象调用,例如,DCOM、RPC或.NET远程处理

∙基于消息的通信,例如,Web服务调用和HTTP请求。

∙分布式事务处理。

许多因素决定了网络服务对应用程序请求的响应速度,其中包括请求的性质、网络滞后时间、连接的可靠性和带宽、单个服务或多个服务的繁忙程度。

这种不可预测性可能会引起单线程应用程序的响应问题,而多线程处理常常是一种好的解决方案。

应该为网络上的所有通信创建针对UI线程的单独线程,然后在接收到响应时将数据传送回UI线程。

为网络通信创建单独的线程并不总是必要的。

如果应用程序通过网络进行异步通信,例如使用MicrosoftWindows消息队列(也称为MSMQ),则在继续执行之前,它不会等待响应。

然而,即使在这种情况下,您仍然应该使用单独的线程来侦听响应,并且在响应到达时对其进行处理。

执行本地操作

即使在处理发生在本地的情况下,有些操作也可能花费很长时间,足以对应用程序的响应产生负面影响。

这样的操作包括:

∙图像呈现。

∙数据操纵。

∙数据排序。

∙搜索。

不应该在UI线程上执行诸如此类的操作,因为这样做会引起应用程序中的性能问题。

相反,应该使用额外的线程来异步执行这些操作,防止UI线程阻塞。

在许多情况下,也应该这样设计应用程序,让它报告正在进行的后台操作的进程和成功或失败。

可能还会考虑允许用户取消后台操作以提高可用性。

区分各种优先级的任务

并不是应用程序必须执行的所有任务都具有相同的优先级。

一些任务对时间要求很急,而一些则不是。

在其他的情况中,您或许会发现一个线程依赖于另一个线程上的处理结果。

应该创建不同优先级的线程以反映正在执行的任务的优先级。

例如,应该使用高优先级线程管理对时间要求很急的任务,而使用低优先级线程执行被动任务或者对时间不敏感的任务。

应用程序启动

应用程序在第一次运行时常常必须执行许多操作。

例如,它可能需要初始化自己的状态,检索或更新数据,打开本地资源的连接。

应该考虑使用单独的线程来初始化应用程序,从而使得用户能够尽快地开始使用该应用程序。

使用单独的线程进行初始化可以增强应用程序的响应能力和可用性。

如果确实在单独的线程中执行初始化,则应该通过在初始化完成之后,更新UI菜单和工具栏按钮的状态来防止用户启动依赖于初始化尚未完成的操作。

还应该提供清楚的反馈消息来通知用户初始化的进度。

返回页首

创建和使用线程

在.NETFramework中有几种方法可以创建和使用后台线程。

可以使用ThreadPool类访问由.NETFramework管理的给定进程的线程池,也可以使用Thread类显式地创建和管理线程。

另外,还可以选择使用委托对象或者Web服务代理来使非UI线程上发生特定处理。

本节将依次分析各种不同的方法,并推荐每种方法应该在何时使用。

使用ThreadPool类

到现在为止,您可能会认识到许多应用程序都会从多线程处理中受益。

然而,线程管理并不仅仅是每次想要执行一个不同的任务就创建一个新线程的问题。

有太多的线程可能会使得应用程序耗费一些不必要的系统资源,特别是,如果有大量短期运行的操作,而所有这些操作都运行在单独线程上。

另外,显式地管理大量的线程可能是非常复杂的。

线程池化技术通过给应用程序提供由系统管理的辅助线程池解决了这些问题,从而使得您可以将注意力集中在应用程序任务上而不是线程管理上。

在需要时,可以由应用程序将线程添加到线程池中。

当CLR最初启动时,线程池没有包含额外的线程。

然而,当应用程序请求线程时,它们就会被动态创建并存储在该池中。

如果线程在一段时间内没有使用,这些线程就可能会被处置,因此线程池是根据应用程序的要求缩小或扩大的。

注每个进程都创建一个线程池,因此,如果您在同一个进程内运行几个应用程序域,则一个应用程序域中的错误可能会影响相同进程内的其他应用程序域,因为它们都使用相同的线程池。

线程池由两种类型的线程组成:

∙辅助线程。

辅助线程是标准系统池的一部分。

它们是由.NETFramework管理的标准线程,大多数功能都在它们上面执行。

∙完成端口线程.这种线程用于异步I/O操作(通过使用IOCompletionPortsAPI)。

注,如果应用程序尝试在没有IOCompletionPorts功能的计算机上执行I/O操作,它就会还原到使用辅助线程。

对于每个计算机处理器,线程池都默认包含25个线程。

如果所有的25个线程都在被使用,则附加的请求将排入队列,直到有一个线程变得可用为止。

每个线程都使用默认堆栈大小,并按默认的优先级运行。

下面代码示例说明了线程池的使用。

privatevoidThreadPoolExample()

{

WaitCallbackcallback=newWaitCallback(ThreadProc);

ThreadPool.QueueUserWorkItem(callback);

}

在前面的代码中,首先创建一个委托来引用您想要在辅助线程中执行的代码。

.NETFramework定义了WaitCallback委托,该委托引用的方法接受一个对象参数并且没有返回值。

下面的方法实现您想要执行的代码。

privatevoidThreadProc(ObjectstateInfo)

{

//Dosomethingonworkerthread.

}

可以将单个对象参数传递给ThreadProc方法,方法是将其指定为QueueUserWorkItem方法调用中的第二个参数。

在前面的示例中,没有给ThreadProc方法传递参数,因此stateInfo参数为空。

在下面的情况下,使用ThreadPool类:

∙有大量小的独立任务要在后台执行。

∙不需要对用来执行任务的线程进行精细控制。

使用Thread类

使用Thread类可以显式管理线程。

这包括CLR创建的线程和进入托管环境执行代码的CLR以外创建的线程。

CLR监视其进程中曾经在.NETFramework内执行代码的所有线程,并且使用Thread类的实例来管理它们。

只要有可能,就应该使用ThreadPool类来创建线程。

然而,在一些情况下,您还是需要创建并管理您自己的线程,而不是使用ThreadPool类。

在下面的情况下,使用Thread对象:

∙需要具有特定优先级的任务。

∙有可能运行很长时间的任务(这样可能阻塞其他任务)。

∙需要确保只有一个线程可以访问特定的程序集。

∙需要有与线程相关的稳定标识。

Thread对象包括许多属性和方法,它们可以帮助控制线程。

可以设置线程的优先级,查询当前的线程状态,中止线程,临时阻塞线程,并且执行许多其他的线程管理任务。

下面的代码示例演示了如何使用Thread对象创建并启动一个线程。

staticvoidMain()

{

System.Threading.ThreadworkerThread=

newSystem.Threading.Thread(SomeDelegate);

workerThread.Start();

}

publicstaticvoidSomeDelegate(){Console.WriteLine("Dosomework.");}

在这个示例中,SomeDelegate是一个ThreadStart委托—指向将要在新线程中执行的代码的引用。

Thread.Start向操作系统提交请求以启动线程。

如果采用这种方式实例化一个新线程,就不可能向ThreadStart委托传递任何参数。

如果需要将一个参数传递给要在另一个线程中执行的方法,应该用所需的方法签名创建一个自定义委托并异步调用它。

有关自定义委托的详细信息,请参阅本章后面的“使用委托”部分。

如果需要从单独的线程中获得更新或结果,可以使用回调方法—一个委托,引用在线程完成工作之后将要调用的代码—这就使得线程可以与UI交互。

有关详细信息,请参阅本章后面的“使用任务处理UI线程和其他线程之间的交互”部分。

使用委托

委托是指向方法的引用(或指针)。

在定义委托时,可以指定确切的方法签名,如果其他的方法想要代表该委托,就必须与该签名相匹配。

所有委托都可以同步和异步调用。

下面的代码示例展示了如何声明委托。

这个示例展示了如何将一个长期运行的计算实现为一个类中的方法。

delegatestringLongCalculationDelegate(intcount);

如果.NETFramework遇到像上面一样的委托声明,就隐式声明了一个从MultiCastDelegate类继承的隐藏类,正如下面的代码示例中所示。

ClassLongCalculationDelegate:

MutlicastDelegate

{

publicstringInvoke(count);

publicvoidBeginInvoke(intcount,AsyncCallbackcallback,

objectasyncState);

publicstringEndInvoke(IAsyncResultresult);

}

委托类型LongCalculationDelegate用于引用接受单个整型参数并返回一个字符串的方法。

下面的代码示例举例说明了一个这种类型的委托,它引用带有相关签名的特定方法。

LongCalculationDelegatelongCalcDelegate=

newLongCalculationDelegate(calculationMethod);

在本示例中,calculationMethod是实现您想要在单独线程上执行的计算的方法的名称。

可以同步或异步调用委托实例所引用的方法。

为了同步调用它,可以使用下面的代码。

stringresult=longCalcDelegate(10000);

该代码在内部使用上面的委托类型中定义的Invoke方法。

因为Invoke方法是同步调用,所以此方法只在调用方法返回之后才返回。

返回值是调用方法的结果。

更常见的情况是,为了防止调用线程阻塞,您将选择通过使用BeginInvoke和B>EndInvoke方法来异步调用委托。

异步委托使用.NETFramework中的线程池化功能来进行线程管理。

.NETFramework实现的标准异步调用模式提供BeginInvoke方法来启动线程上所需的操作,并且它提供EndInvoke方法来允许完成异步操作以及将任何得到的数据传送回调用线程。

在后台处理完成之后,可以调用回调方法,其中,可以调用EndInvoke来获取异步操作的结果。

当调用BeginInvoke方法时,它不会等待调用完成;相反,它会立即返回一个IAsyncResult对象,该对象可以用来监视该调用的进度。

可以使用IAsyncResult对象的WaitHandle成员来等待异步调用完成,或使用IsComplete成员轮询是否完成。

如果在调用完成之前调用EndInvoke方法,它就会阻塞,并且只在调用完成之后才返回。

然而,您应该慎重,不要使用这些技术来等待调用完成,因为它们可能阻塞UI线程。

一般来说,回调机制是通知调用已经完成的最好方式。

异步执行委托引用的方法

1.定义代表长期运行的异步操作的委托,如下面的示例所示:

2.delegatestringLongCalculationDelegate(intcount);

3.定义一个与委托签名相匹配的方法。

下面的示例中的方法模拟需要消耗较多时间的操作,方法是使线程返回之前睡眠count毫秒。

4.privatestringLongCalculation(intcount)

5.{

6.Thread.Sleep(count);

7.returncount.ToString();

8.}

9.定义与.NETFramework定义的AsyncCallback委托相对应的回调方法,如下面的示例所示。

10.privatevoidCallbackMethod(IAsyncResultar)

11.{

12.//Retrievetheinvokingdelegate.

13.LongCalculationDelegatedlgt=(LongCalculationDelegate)ar.AsyncState;

14.//CallEndInvoketoretrievetheresults.

15.stringresults=dlgt.EndInvoke(ar);

16.}

17.创建一个委托实例,它引用您想要异步调用的方法,并且创建一个AsyncCallback委托来引用回调方法,如下面的代码示例所示。

18.LongCalculationDelegatelongCalcDelegate=

19.newLongCalculationDelegate(calculationMethod);

20.AsyncCallbackcallback=newAsyncCallback(CallbackMethod);

21.从调用线程中开始异步调用,方法是调用引用您想要异步执行的代码的委托中的BeginInvoke方法。

2

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 理化生

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1